Executando verificação de segurança...
4

Funcionalidades do JDK 8 (Java 8) - Parte XI - API de Datas

API de datas

No Java 8 surgiu o pacote java.time que nos trouxe uma nova API de datas. Essa API foi inspirada parcialmente no Joda Time que é uma API que já existe há algum tempo e é bem melhor para trabalhar com datas. O Joda-Time é uma biblioteca open source bastante conhecida.

Datas de Modo Mais Fluente

Vamos imaginar que você precise criar uma data com um mês a partir da data atual, no Java 7:

Calendar mesQueVem = Calendar.getInstance();
mesQueVem.add(Calendar.MONTH, 1);

Com a nova API de datas o código fica bem mais moderno utilizando sua interface fluente

LocalDate mesQueVem = LocalDate.now().plusMonths(1);

Além de plusMonths() ainda podemos adicionar dias plusDays(), anos plusYears() e por aí vai.

De forma semelhante podemos decrementar os valores

LocalDate mesPassado = LocalDate.now().minusMonths(1);

A classe LocalDate representa uma data sem time zone, algo como 25-01-1988, se as informações de horário forem importantes usamos a calsse LocalDateTime

LocalDateTime mesQueVem = LocalDateTime.now().plusMonths(1);
LocalDateTime mesPassado = LocalDateTime.now().minusMonths(1);

Com LocalDateTime a saída ficará mais ou menos assim:

25-01-1988T15:01.345

Outra forma de criar uma data com horário específico seria utilizar o método atTime() da classe LocalDate

LocalDateTime dateTime = LocalDate.now().atTime(12, 0);

Note que criamos a partir de LocalDate, porém atTime() retorna um LocalDateTime.

Assim como foi feito com o método atTime() podemo combinar os diferentes modelos

LocalTime agora = LocalTime.now();
LocalDate hoje = LocalDate.now();
LocalDateTime dataEHora = hoje.atTime(agora);

Se precisarmos de um horário baseado em um TimeZone, podemos contar com o método atZone()

LocalTime agora = LocalTime.now();
LocalDate hoje = LocalDate.now();
LocalDateTime dataEHora = hoje.atTime(agora);

ZonedDateTime dataComHoraETimeZone = dataEHora.atZone(ZoneId.of("America/Sao_Paulo"));

Podemos converter esses objetos para outras medidas de tempo utilizando os métodos to, por exemplo toLocalDateTime().

LocalDateTime dataEHoraSemTimeZone = dataComHoraETimeZone.toLocalDateTime();

As classes dessa nova API ainda contam com métodos estáticos of, que são factories methods para construção de novas instâncias

LocalDate date = LocalDate.of(1988, 01, 25);
LocalDateTime dataTime = LocalDateTime.of(1988, 01, 25, 10, 30);

Podemos converter Strings em datas com o método parse() porém esse método exige que a String esteja em um formato correto YYYY-MM-DD

LocalDate date = LocalDate.parse("1988-01-25");
System.out.println(date);

O modelo do java.time é imutável, cada operação devolve um novo valor nunca alterando o valor interno dos horários, datas e intervalos utilizados na operação. De modo semelhante aos setters, os modelos imutáveis possuem métodos with para alterar uma data. Por exemplo:

LocalDate date = LocalDate.now().withYear(1988);
System.out.println(date.getYear());

Aqui é criada uma data com o ano atual, por conta do now() o ano é 2023 nesse caso, depois com o o withYear() o ano é alterado para 1988 !

Outros comportamentos essenciais e interessantes é poder saber se alguma medida de tempo acontece antes, depois ou ao mesmo tempo que outra, para essa finalidade temos os métodos is

LocalDate hoje = LocalDate.now();
LocalDate amanha = LocalDate.now().plusDays(1);

System.out.println(hoje.isBefore(amanha));
System.out.println(hoje.isAfter(amanha));
System.out.println(hoje.isEqual(amanha));

Nesse exemplo apenas isBefore() vai retornar true.

Para comparar datas iguais mas em time zones diferentes também temos um método chamado isEqual() já que o `equals()`` não funcionaria.

ZonedDateTime tokyo = ZonedDateTime.of(1988, 5, 13, 10, 30, 0, 0, ZoneId.of("Asia/Tokyo"));

ZonedDateTime saoPaulo = ZonedDateTime.of(1988, 5, 13, 10, 30, 0, 0, ZoneId.of("America/Sao_Paulo"));

System.out.println(tokyo.isEqual(saoPaulo));

Para que esse resultado seja true precisaríamos acertar a diferença de 12 horas entre as duas time zones

ZonedDateTime tokyo = ZonedDateTime .of(1988, 5, 13, 10, 30, 0, 0, ZoneId.of("Asia/Tokyo"));

ZonedDateTime saoPaulo = ZonedDateTime.of(1988, 5, 13, 10, 30, 0, 0, ZoneId.of("America/Sao_Paulo"));

Uma curiosidade, experimente mudar o mês para 1 (janeiro) e você notará que o isEqual() falha, se você debugar vai notar o por que:

Um objeto ZonedDateTime tem alguns atributos, a saber:

  • dateTime: o date time propriamente dito algo como 1988-05-13T10:30+09:00[Asia/Tokyo]

  • offset: O deslocamento em horas a partir de UTC/Greenwich, algo como +09:00

  • zone: a zona propriamente dita, algo como Asia/Tokyo

Se você prestar atenção ao objeto gerado para São Paulo, vai notar que no mês de maio o offset é -03:00 mas em janeiro é -02:00, isso ocorre por que, nesse momento momento em que o horário de verão está em vigor no Brasil, o offset pode ser diferente. Durante o horário de verão, o Brasil pode mudar para o fuso horário de "-02:00" para aproveitar mais a luz do dia.

Sendo assim para que esse mesmo código funciona é preciso compensar essa 1 hora

ZonedDateTime tokyo = ZonedDateTime.of(1988, 1, 13, 10, 30, 0, 0, ZoneId.of("Asia/Tokyo"));

ZonedDateTime saoPaulo = ZonedDateTime.of(1988, 1, 13, 10, 30, 0, 0, ZoneId.of("America/Sao_Paulo"));

// compensando o horário de verão
tokyo = tokyo.plusHours(11);

System.out.println(tokyo.isEqual(saoPaulo));

A API possui ainda outros modelos que facilitam bastante o nosso trabalho como as classes MonthDay, Year e YearMonth, por exemplo:

System.out.println("hoje é dia: " + MonthDay.now().getDayOfMonth());

Podemos por exemplo obter a o mês de uma data

LocalDate hoje = LocalDate.now();
YearMonth yearMonth = YearMonth.from(hoje);
System.out.println(yearMonth.getMonth() + " " + yearMonth.getYear());

Enums vs Constantes

Calendar utilizava constantes para representar unidades temporais, a nova API faz isso por meio de enums, como exemplo podemos citar a enum Month, onde cada valor tem um valor inteiro e representa o mes, seguindo o intervalo de 1 (Janeiro), 2 (Fevereiro) até 12 (Dezembro). Você não precisa mas trabalhar com essas enums deixa seu código muito mais legível.

System.out.println(LocalDate.of(1988, 01, 25));
System.out.println(LocalDate.of(1988, Month.JANUARY, 25));

Outra vantagem de se utilizar as enums é a de poder contar com seus métodos auxiliares, por exemplo o firstMonthOfQuarter() para consultar o mês correspondente ao primeiro mês deste trimestre.

System.out.println(Month.NOVEMBER.firstMonthOfQuarter());
System.out.println(Month.JANUARY.plus(2));
System.out.println(Month.JANUARY.minus(1));

Note que ao imprimir o nome de um mês vamos sempre ver o mês em ingles, por exemplo JANUARY, JULY e por aí vai. Para obter o mes em outra configuração podemos contar com o Locales

Locale pt = new Locale("pt");
System.out.println(Month.JANUARY.getDisplayName(TextStyle.FULL, pt));

O argumento TextStyle é uma enum que informa o estilo de formatação:

Os valores possíveis são:

Locale pt = new Locale("pt");

System.out.println(Month.JANUARY.getDisplayName(TextStyle.FULL, pt)); // Janeiro

System.out.println(Month.JANUARY.getDisplayName(TextStyle.FULL_STANDALONE, pt)); // 1

System.out.println(Month.JANUARY.getDisplayName(TextStyle.NARROW, pt)); // J

System.out.println(Month.JANUARY.getDisplayName(TextStyle.NARROW_STANDALONE, pt)); // 1

System.out.println(Month.JANUARY.getDisplayName(TextStyle.SHORT, pt)); // jan

System.out.println(Month.JANUARY.getDisplayName(TextStyle.SHORT_STANDALONE, pt)); // 1

Outro método interessante introduzido na api java.time foi o dayOfWeek() com ele podemos representar facilmente um dia da semana.

Formatando datas

Para formatar datas podemos fazer algo como

LocalDateTime agora = LocalDateTime.now();

String dataFormatada = agora.format(DateTimeFormatter.ISO_LOCAL_TIME);

System.out.println(dataFormatada);

A saída seria algo como 03:12:31.428 ou seja usando o pattern hh:mm:ss.ms

O DateTimeFormatter possui diversas outras opções, mas vamos dizer que tenhamos um formato próprio ou queremos criar algo que não exista ainda, uma das formas seria utilizar o método ofPattern(), que recebe uma String como parâmetro

LocalDateTime agora = LocalDateTime.now();

String dataFormatada = agora.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));

System.out.println(dataFormatada);

Esse método ainda possui uma sobrecarga que além do pattern pode receber um Locale.

Lidando com datas inválidas

Qual a saída desse código ?

Calendar calendar = Calendar.getInstance();
calendar.set(2014, Calendar.FEBRUARY, 30);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MM/yyyy");
System.out.println(simpleDateFormat.format(calendar.getTime()));

Se você respondeu 30 de Fevereiro de 2014 você errou feio, errou rude ! Porém ao executar o código, veja que não há se quer um alerta sobre o o problema eminente, o Calendar simplesmente ajusta a data para 02/03/2014 e vida que segue ! Nem é preciso dizer que isso poderia causar diversos tipos de prejuízos né ?

Agora tente fazer o mesmo com a nova API de datas

LocalDate.of(2014, Month.FEBRUARY, 30);

E você receberá uma belíssima exception

Exception in thread "main" java.time.DateTimeException: Invalid date 'FEBRUARY 30'
	at java.time.LocalDate.create(LocalDate.java:431)
	at java.time.LocalDate.of(LocalDate.java:249)
	at br.com.jorgerabellodev.lambadas.datas.Main.main(Main.java:9)

O mesmo acontece com horários

LocalDateTime hour = LocalDate.now().atTime(25, 12);

Esse código produzirá um java.time.DateTimeException

Exception in thread "main" java.time.DateTimeException: Invalid value for HourOfDay (valid values 0 - 23): 25
	at java.time.temporal.ValueRange.checkValidValue(ValueRange.java:311)
	at java.time.temporal.ChronoField.checkValidValue(ChronoField.java:703)
	at java.time.LocalTime.of(LocalTime.java:296)
	at java.time.LocalDate.atTime(LocalDate.java:1724)
	at br.com.jorgerabellodev.lambadas.datas.Main.main(Main.java:10)

Duração e período

Só quem já precisou trabalhar com diferença de alguma medida de tempo no Java até a versão 7 sabe o inferno que era fazer isso, apenas para compartilhar um pouco do sofrimento aqui vai um código que poderia ser utilizado pra tal finalidade caso você tenha tido a sorte de não ter precisado fazer isso até hoje.

 Calendar now = Calendar.getInstance();

Calendar anotherDate = Calendar.getInstance();
anotherDate.set(1988, Calendar.JANUARY, 25);

long difference = now.getTimeInMillis() - anotherDate.getTimeInMillis();

long milliSecondsInADay = 1000 * 60 * 60 * 24;

long dias = difference / milliSecondsInADay;

System.out.println(dias);

O problema é resolvido porém trabalhar com diferença entre datas utilizando milisegundos pode nem sempre ser uma boa ideia !

Com a nova API de datas, podemos fazer a mesma coisa de forma mais segura e e simples

LocalDate now = LocalDate.now();

LocalDate anotherDate = LocalDate.of(1988, Month.JANUARY, 25);

long dias = ChronoUnit.DAYS.between(anotherDate, now);

System.out.println(dias);

A enum ChronoUnit está presente no pacote java.time.temporal e possui uma representação para cada medida de tempo e data, além de vários métodos auxiliares que facilitam extrair informações úteis de datas

LocalDate now = LocalDate.now();

LocalDate anotherDate = LocalDate.of(1988, Month.JANUARY, 25);

long dias = ChronoUnit.DAYS.between(anotherDate, now);

long meses = ChronoUnit.MONTHS.between(anotherDate, now);

long anos = ChronoUnit.YEARS.between(anotherDate, now);

System.out.printf("%s dias, %s meses e %s anos", dias, meses, anos);

Agora se precisarmos obter os dias, meses e anos entre duas datas, podemos utilizar Period, essa classe da API também possui o método between() que recebe duas instâncias de LocalDate

LocalDate now = LocalDate.now();
LocalDate anotherDate = LocalDate.of(1988, Month.JANUARY, 25);

Period period = Period.between(anotherDate, now);

System.out.printf("%s dias, %s meses e %s anos",
            period.getDays(), period.getMonths(), period.getYears());

Pode ser que precisemos criar um período entre horas, minutos e segundos, nesse caso Period não servirá, para essa finalidade vamos utilizar Duration

LocalDateTime now = LocalDateTime.now();

LocalDateTime aFewHours = LocalDateTime.now().plusHours(4);

Duration difference = Duration.between(now, aFewHours);

if (difference.isNegative()) {
    difference = difference.negated();
}

System.out.printf("%s horas, %s minutos, %s segundos",
            difference.toHours(), difference.toMinutes(), difference.getSeconds());

Com esse último artigo, encerro a demonstração das features do Java 8 e espero que você que está lendo, tenha compreendido um pouco melhor essas funcionalidades, além disso espero que você escreve código de forma mais fluída e com menos sofrimento !

Carregando publicação patrocinada...
1

Caramba SeuJorge! Eu ainda não tinha tomado nota dessa série fenomenal

Obrigado por compartilhar conhecimento e trazer um pouco do Java para a plantaforma, afinal, o java é como o diabo: "você pode não acreditar nele, mas ele acredita em você!" kkkkkkk

Com certeza irei ler o restante e começando do inicio, parabéns pelo conteudo e continua postando conteúdos assim que soma bastante, principalmente para devs perdidos como eu haha.

1

Muito bom! Alguns detalhes para complementar:

Os métodos withYear, withMonth, etc, na verdade não modificam a data. As classes do java.time são imutáveis, então estes métodos sempre retornam outra instância com o valor modificado.

Por isso se vc fizer:

LocalDate data = LocalDate.now();
data.withYear(2000);
System.out.println(data);

Não vai mudar o ano para 2000, vai continuar imprimindo a data atual. Isso porque withYear retornou outra instância de LocalDate, que não foi atribuída a nenhuma variável, e portanto "se perdeu".

Para obter a data com ano alterado, deve-se usar o valor retornado:

LocalDate data = LocalDate.now();
LocalDate outra = data.withYear(2000);
System.out.println(outra);

O mesmo vale para os métodos plusXXX e minusXXX, eles sempre retornam outra instância com o resultado.

Outro ponto é que não precisaria ficar chamando now toda hora, então em vez disso:

LocalDate hoje = LocalDate.now();
LocalDate amanha = LocalDate.now().plusDays(1);

Poderia ser isso:

LocalDate hoje = LocalDate.now();
LocalDate amanha = hoje.plusDays(1);

Na maioria dos casos o resultado será o mesmo, mas tem um corner case: o código pode rodar muito próximo da meia-noite, então o primeiro now retorna um dia e o segundo retorna outro. O resultado é que amanha acabará com uma data dois dias à frente de hoje.


Quanto a este exemplo:

LocalDate hoje = LocalDate.now();
YearMonth yearMonth = YearMonth.from(hoje);
System.out.println(yearMonth.getMonth() + " " + yearMonth.getYear());

Se a ideia era apenas obter o mês e ano, não precisaria usar YearMonth, poderia obter diretamente:

LocalDate hoje = LocalDate.now();
System.out.println(hoje.getMonth() + " " + hoje.getYear());

O uso de YearMonth é quando vc precisa apenas desses dois campos (por exemplo, para data de expiração de cartão de crédito, que possui somente ano e mês).


Para formatação, eu tenho preferido usar uuuu em vez de yyyy para o ano. O motivo disto é que yyyy não funciona em caso de datas antes de Cristo. Para a maioria dos casos não faz diferença, pois ambos funcionam, mas nos casos em que faz diferença, o uuuu deve ser usado. Mais detalhes nesta resposta (em inglês).

Quanto a 30 de fevereiro, de fato não dá para criar usando LocalDate.of, mas e se tentarmos fazer o parsing?

DateTimeFormatter parser = DateTimeFormatter.ofPattern("dd/MM/uuuu");
LocalDate data = LocalDate.parse("30/02/2020", parser);
System.out.println(data); // 2020-02-29

Tentei fazer o parsing de 30 de fevereiro, e a data foi ajustada para o dia 29. Basicamente, é feito um "arredondamento" para o último dia válido do mês (lembrando que 2020 é ano bissexto: se não fosse, o ajuste seria feito para o dia 28).

Se a ideia é não aceitar datas inválidas e não fazer tal ajuste, basta mudar o ResolverStyle:

DateTimeFormatter parser = DateTimeFormatter.ofPattern("dd/MM/uuuu")
    .withResolverStyle(ResolverStyle.STRICT);
LocalDate data = LocalDate.parse("30/02/2020", parser);

Agora dá erro, porque a data é inválida:

java.time.format.DateTimeParseException: Text '30/02/2020' could not be parsed: Invalid date 'FEBRUARY 30'

Basicamente, existem 3 modos diferentes de tratar datas inválidas:

O modo LENIENT permite datas inválidas e faz ajustes automáticos. Por exemplo, 31/06/2017 é ajustado para 01/07/2017. Além disso, este modo aceita valores fora dos limites definidos para cada campo, como o dia 32, mês 15, etc. Por exemplo, 32/15/2017 é ajustado para 01/04/2018.

O modo SMART também faz alguns ajustes quando a data é inválida, então 31/06/2017 é interpretado como 30/06/2017. A diferença para LENIENT é que este modo não aceita valores fora dos limites dos campos (mês 15, dia 32, etc), então 32/15/2017 dá erro (lança um DateTimeParseException). É o modo default quando você cria um DateTimeFormatter.

O modo STRICT é o mais restrito: não aceita valores fora dos limites e nem faz ajustes quando a data é inválida, portanto 31/06/2017 e 32/15/2017 dão erro (lançam um DateTimeParseException).


Sobre o exemplo de Duration, só tem um pequeno detalhe na hora de mostrar os dados. Considere este exemplo:

// duração de 10 horas, 35 minutos e 20 segundos
Duration difference = Duration.ofHours(10).plusMinutes(35).plusSeconds(20);
System.out.printf("%s horas, %s minutos, %s segundos",
    difference.toHours(), difference.toMinutes(), difference.getSeconds());

A saída deveria ser "10 horas, 35 minutos e 20 segundos", mas na verdade foi:

10 horas, 635 minutos, 38120 segundos

Isso porque toMinutes retorna a quantidade total de minutos (o mesmo vale para getSeconds). Se quer quebrar em partes, pode usar os métodos toXXXPart, disponíveis a partir do Java 9:

Duration difference = Duration.ofHours(10).plusMinutes(35).plusSeconds(20);

// atenção: toXXXPart só funciona a partir do Java 9
System.out.printf("%s horas, %s minutos, %s segundos",
        difference.toHoursPart(), difference.toMinutesPart(), difference.toSecondsPart());

// para Java 8, tem que fazer na mão
long secs = difference.getSeconds();
long hours = secs / 3600;
secs %= 3600;
long mins = secs / 60;
secs %= 60;
System.out.printf("%s horas, %s minutos, %s segundos", hours, mins, secs);

Leitura complementar:

1

Graças a deus surgiu essa API, por que antigamente era horrível lidar com datas eu acho (experiência de ter precisaodo usar java velho na disciplina da faculdade)

1

Só uma duvida que eu fiquei. O que, especificamente é "um ponto no tempo"? Vi citar em muitos artigos que X classe não é um ponto no tempo, enquanto Y é, sendo que ambas, necessariamente, mexem em datas e datas nada mais são do que a abstração de tempo para uma convenção humana.

Fora essa pergunta, a outra seria a diferença entre ChronoUnit e ChronoField, por isso fiz a pergunta anterior, uma mede a quantidade de tempo enquanto a outra é, necessariamente, um ponto no tempo, acabando me confundindo entre ambas.

1

Excelentes perguntas, vou tentar responder e se ficarem dúvidas por favor me diga.

Quanto a "ponto no tempo" quis dizer que são classes que marcam um determinado periodo de tempo em uma linha do tempo, de X a Y e não uma data avulsa, mas confesso que relendo e pensando fiquei intrigado e vou melhorar os textos atualizando eles para ser mais claro a respeito, muito obrigado pelo feedback ^^.

Agora quato a ChronoUnit e ChronoField:

ChronoUnit é uma enumeração que representa unidades de tempo padrão, como anos, meses, dias, horas, minutos, segundos, entre outros.
É usada para realizar cálculos entre datas e períodos, como adicionar ou subtrair uma quantidade específica de unidades de tempo a uma data, por exemplo:

LocalDate.now().plus(3, ChronoUnit.MONTHS) 

Isso adicionará 3 meses à data atual.

ChronoField é uma enumeração que representa campos específicos de uma data ou hora, como ano, mês, dia do mês, hora, minuto, segundo, entre outros. É utilizada para acessar e manipular os valores individuais desses campos em uma data ou hora. Por exemplo:

LocalDate.now().get(ChronoField.YEAR) 

Isso retorna o ano atual da data.

ChronoUnit é usado para cálculos entre datas, enquanto ChronoField é usado para acessar e manipular componentes específicos da data ou hora.

1

Opa, muito obrigado pela explicação do que significa um ponto no tempo e uma data, clareou a mente. Um tem conhecimento do todo (a linha do tempo) e o significado de si mesmo neste todo, o outro nem sequer conhece o todo ou se identifica nele.

Agora sobre ChronoUnit e ChronoField específicamente, o cálculo em si não seria uma forma de acessar e manipular data ou hora? Fazendo ambos, necessariamente, ter o mesmo objetivo?

E referente a nomenclatura "unidade de tempo padrão" e "campos específicos de uma data ou hora" me parecem muito apenas mudar o nome para o que, em resultado, é a mesma coisa, ambos são enumerações que contém anos, meses, dias, horas, etc. Com a adição do ChronoField ter um detalhamento ainda maior tendo os dias no mes, semanas do ano, etc. Ou seja, um fazendo exatamente a mesma coisa que o outro só que com mais enriquecimento de detalhes. (Apesar de parecer uma afirmação, é só uma tentativa de explicar a minha linha de raciocínio que, inclusive, está me confundindo)

1

Opa boa boa excelentes pontos esses, me fizeram pensar em algumas coisas:

De fato ChronoUnit e ChronoField são descritas de forma bem parecida, eu fui dar uma olhada no JavaDoc delas e saca só:

/**
 * A standard set of fields.
 * <p>
 * This set of fields provide field-based access to manipulate a date, time or date-time.
 * The standard set of fields can be extended by implementing {@link TemporalField}.
 * <p>
 * These fields are intended to be applicable in multiple calendar systems.
 * For example, most non-ISO calendar systems define dates as a year, month and day,
 * just with slightly different rules.
 * The documentation of each field explains how it operates.
 *
 * @implSpec
 * This is a final, immutable and thread-safe enum.
 *
 * @since 1.8
 */
public enum ChronoField implements TemporalField {
/**
 * A standard set of date periods units.
 * <p>
 * This set of units provide unit-based access to manipulate a date, time or date-time.
 * The standard set of units can be extended by implementing {@link TemporalUnit}.
 * <p>
 * These units are intended to be applicable in multiple calendar systems.
 * For example, most non-ISO calendar systems define units of years, months and days,
 * just with slightly different rules.
 * The documentation of each unit explains how it operates.
 *
 * @implSpec
 * This is a final, immutable and thread-safe enum.
 *
 * @since 1.8
 */
public enum ChronoUnit implements TemporalUnit {

A ChronoField é um "conjunto de campos padrão" e ChronoUnit é descrita como "um conjunto padrão de unidades de período de datas".

De fato por descrição são praticamente a mesma coisa.

Mas vamos imaginar que eu quero somar 10 dias na data atual, eu faria algo mais ou menos assim:

LocalDate value = LocalDate.now().plus(10, ChronoUnit.DAYS);

Como a assinatura do método plus() espera receber um TemporalAmount ou um valor inteiro e um TemporalUnit então só é possível utilizar ChronoUnit que implementa a interface (é um) TemporalUnit.

Da mesma forma, vamos dizer que eu queira saber em que mês estamos, eu teria de fazer

LocalDate.now().get(ChronoField.MONTH_OF_YEAR);

Nesse caso o método get() espera receber um TemporalField, logo não é possível utilizar ChronoUnit e por isso utilizamos ChronoField que implementa a interface (é um) TemporalField.

Quanto a semântica, penso o seguinte:

ChronoUnit
Uma unidade deve ser utilizada para medir uma quantidade de tempo - anos, meses, dias, horas, minutos, segundos. Por exemplo, o segundo é uma unidade do S.I.

ChronoField
Por outro lado, os campos são como os humanos geralmente se referem ao tempo, que é em partes. Se você olhar para um relógio digital, os segundos contam de 0 a 59 e depois voltam para 0 novamente.

Este é um campo - "segundo do minuto" neste caso, formado pela contagem de segundos dentro de um minuto.

Da mesma forma, os dias são contados dentro de um mês e os meses dentro de um ano. Para definir um ponto completo na linha do tempo, você precisa ter um conjunto de campos vinculados, por exemplo:

  • segundo de minuto
  • minuto-a-hora
  • hora do dia
  • dia do mês
  • mês do ano
  • ano (-de-para-sempre)

A API ChronoField expõe as duas partes do segundo do minuto.Podemos utilizar getBaseUnit() para obter "segundos" e getRangeUnit() para obter "minutos".

A parte Chrono do nome refere-se ao fato de que as definições são cronologicamente neutras. Especificamente, isso significa que a unidade ou campo tem significado apenas quando associado a um sistema de calendário ou cronologia. Um exemplo disso é a cronologia copta, onde há 13 meses em um ano. Apesar de ser diferente do sistema de calendário civil/ISO comum, a constante ChronoField.MONTH_OF_YEAR ainda pode ser usada.

As interfaces TemporalUnit e TemporalField fornecem a abstração de nível mais alto, permitindo que unidades/campos que não são cronologicamente neutros sejam adicionados e processados.

Não sei se ficou mais claro ou mais confuso, mas pelo menos acho que eu entendi melhor um pouco sobre essa duas enums e sua utilização.

Penso que talvez pudessem ter feito uma única enum pra tudo, porém acredito que tiverem alguns motivos pra ter as duas, sendo o primeiro deles querer separar unidades de campos para que aquele que lê o código possa entender mais rapidamente do que se trata aquele código.