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 !
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.
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:
- Como migrar de Date e Calendar para a nova API de datas no Java 8?
- Por que eu deveria ou não usar a Joda-Time? (Joda-Time é uma API de datas que por muito tempo foi a melhor alternativa para
Date
eCalendar
, e serviu de inspiração para ojava.time
- inclusive, ambas as API's foram feitas pela mesma pessoa) - Livro: Datas e horas - Conceitos fundamentais e as APIs do Java (PITCH: eu que escrevi :-D E até 28 de julho, tem 12% de desconto, usando o cupom
CASADOCODIGO12
)
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.
* 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}. *
* 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 { ``` ```java /** * A standard set of date periods units. *
* 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}. *
* 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: ```java 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 ```java 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.