@@ -650,3 +650,328 @@ class Person {
650650
651651## 枚举类
652652
653+ ### enum
654+
655+ 为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用`enum`来定义枚举类:
656+
657+ ```java
658+ // enum
659+ public class Main {
660+ public static void main(String [] args) {
661+ Weekday day = Weekday . SUN ;
662+ if (day == Weekday . SAT || day == Weekday . SUN ) {
663+ System . out. println(" Work at home!" );
664+ } else {
665+ System . out. println(" Work at office!" );
666+ }
667+ }
668+ }
669+
670+ enum Weekday {
671+ SUN , MON , TUE , WED , THU , FRI , SAT ;
672+ }
673+ ```
674+
675+ 注意到定义枚举类是通过关键字`enum`实现的,我们只需依次列出枚举的常量名。
676+
677+ 和`int `定义的常量相比,使用`enum`定义枚举有如下好处:
678+
679+ 首先,`enum`常量本身带有类型信息,即`Weekday . SUN `类型是`Weekday `,编译器会自动检查出类型错误。例如,下面的语句不可能编译通过:
680+
681+ ```java
682+ int day = 1 ;
683+ if (day == Weekday .SUN ) { // Compile error: bad operand types for binary operator '=='
684+ }
685+ ```
686+
687+ 其次,不可能引用到非枚举的值,因为无法通过编译。
688+
689+ 最后,不同类型的枚举不能互相比较或者赋值,因为类型不符。例如,不能给一个`Weekday `枚举类型的变量赋值为`Color `枚举类型的值:
690+
691+ ```java
692+ Weekday x = Weekday . SUN ; // ok!
693+ Weekday y = Color . RED ; // Compile error: incompatible types
694+ ```
695+
696+ 这就使得编译器可以在编译期自动检查出所有可能的潜在错误。
697+
698+ ### enum的比较
699+
700+ 使用`enum`定义的枚举类是一种引用类型。 引用类型比较,要使用`equals ()`方法,如果使用`==`比较,它比较的是两个引用类型的变量是否是同一个对象。因此,引用类型比较,要始终使用`equals ()`方法,但`enum`类型可以例外。
701+
702+ 这是因为`enum`类型的每个常量在JVM中只有一个唯一实例,所以可以直接用`==`比较:
703+
704+ ```java
705+ if (day == Weekday .FRI ) { // ok!
706+ }
707+ if (day .equals (Weekday .SUN )) { // ok, but more code!
708+ }
709+ ```
710+
711+ ### enum类型
712+
713+ 通过`enum`定义的枚举类,和其他的`class`有什么区别?
714+
715+ 答案是没有任何区别。`enum`定义的类型就是`class`,只不过它有以下几个特点:
716+
717+ - 定义的`enum`类型总是继承自`java.lang. Enum `,且无法被继承;
718+ - 只能定义出`enum`的实例,而无法通过`new `操作符创建`enum`的实例;
719+ - 定义的每个实例都是引用类型的唯一实例;
720+ - 可以将`enum`类型用于`switch`语句。
721+
722+ #### name()
723+
724+ 返回常量名,例如:
725+
726+ ```java
727+ String s = Weekday . SUN. name(); // "SUN"
728+ ```
729+
730+ #### ordinal ()
731+
732+ 返回定义的常量的顺序,从0开始计数,例如:
733+
734+ ```java
735+ int n = Weekday.MON.ordinal (); // 1
736+ ```
737+
738+ 改变枚举常量定义的顺序就会导致`ordinal ()`返回值发生变化。例如:
739+
740+ ```java
741+ public enum Weekday {
742+ SUN , MON , TUE , WED , THU , FRI , SAT ;
743+ }
744+ ```
745+
746+ 和
747+
748+ ```java
749+ public enum Weekday {
750+ MON , TUE , WED , THU , FRI , SAT , SUN ;
751+ }
752+ ```
753+
754+ 的`ordinal`就是不同的。如果在代码中编写了类似`if (x .ordinal ()==1)`这样的语句,就要保证`enum`的枚举顺序不能变。新增的常量必须放在最后。
755+
756+ 有些童鞋会想,`Weekday`的枚举常量如果要和`int`转换,使用`ordinal ()`不是非常方便?比如这样写:
757+
758+ ```java
759+ String task = Weekday.MON.ordinal () + "/ppt";
760+ saveToFile (task );
761+ ```
762+
763+ 但是,如果不小心修改了枚举的顺序,编译器是无法检查出这种逻辑错误的。要编写健壮的代码,就不要依靠`ordinal ()`的返回值。因为`enum`本身是`class`,所以我们可以定义`private `的构造方法,并且,给每个枚举常量添加字段:
764+
765+ ```java
766+ // enum
767+ public class Main {
768+ public static void main(String [] args) {
769+ Weekday day = Weekday . SUN ;
770+ if (day. dayValue == 6 || day. dayValue == 0 ) {
771+ System . out. println(" Work at home!" );
772+ } else {
773+ System . out. println(" Work at office!" );
774+ }
775+ }
776+ }
777+
778+ enum Weekday {
779+ MON (1 ), TUE (2 ), WED (3 ), THU (4 ), FRI (5 ), SAT (6 ), SUN (0 );
780+
781+ public final int dayValue;
782+
783+ private Weekday (int dayValue ) {
784+ this . dayValue = dayValue;
785+ }
786+ }
787+ ```
788+
789+ 这样就无需担心顺序的变化,新增枚举常量时,也需要指定一个`int `值。
790+
791+ 默认情况下,对枚举常量调用`toString ()`会返回和`name ()`一样的字符串。但是,`toString ()`可以被覆写,而`name ()`则不行。我们可以给`Weekday`添加`toString ()`方法:
792+
793+ ```java
794+ // enum
795+ public class Main {
796+ public static void main(String [] args) {
797+ Weekday day = Weekday . SUN ;
798+ if (day. dayValue == 6 || day. dayValue == 0 ) {
799+ System . out. println(" Today is " + day + " . Work at home!" );
800+ } else {
801+ System . out. println(" Today is " + day + " . Work at office!" );
802+ }
803+ }
804+ }
805+
806+ enum Weekday {
807+ MON (1 , " 星期一" ), TUE (2 , " 星期二" ), WED (3 , " 星期三" ), THU (4 , " 星期四" ), FRI (5 , " 星期五" ), SAT (6 , " 星期六" ), SUN (0 , " 星期日" );
808+
809+ public final int dayValue;
810+ private final String chinese;
811+
812+ private Weekday (int dayValue , String chinese ) {
813+ this . dayValue = dayValue;
814+ this . chinese = chinese;
815+ }
816+
817+ @Override
818+ public String toString () {
819+ return this . chinese;
820+ }
821+ }
822+ ```
823+
824+ 覆写`toString ()`的目的是在输出时更有可读性。
825+
826+ ### switch
827+
828+ 最后,枚举类可以应用在`switch`语句中。因为枚举类天生具有类型信息和有限个枚举常量,所以比`int`、`String`类型更适合用在`switch`语句中:
829+
830+ ```java
831+ // switch
832+ public class Main {
833+ public static void main(String [] args) {
834+ Weekday day = Weekday . SUN ;
835+ switch (day) {
836+ case MON :
837+ case TUE :
838+ case WED :
839+ case THU :
840+ case FRI :
841+ System . out. println(" Today is " + day + " . Work at office!" );
842+ break ;
843+ case SAT :
844+ case SUN :
845+ System . out. println(" Today is " + day + " . Work at home!" );
846+ break ;
847+ default :
848+ throw new RuntimeException (" cannot process " + day);
849+ }
850+ }
851+ }
852+
853+ enum Weekday {
854+ MON , TUE , WED , THU , FRI , SAT , SUN ;
855+ }
856+ ```
857+
858+ 加上`default `语句,可以在漏写某个枚举常量时自动报错,从而及时发现错误。
859+
860+ ## record类
861+
862+ 从Java 14 开始,引入了新的`Record `类。我们定义`Record `类时,使用关键字`record`。把上述`Point `类改写为`Record `类,代码如下:
863+
864+ ```java
865+ // Record
866+ public class Main {
867+ public static void main (String [] args ) {
868+ Point p = new Point (123 , 456 );
869+ System . out. println(p. x());
870+ System . out. println(p. y());
871+ System . out. println(p);
872+ }
873+ }
874+
875+ record Point (int x , int y ) {}
876+ ```
877+
878+ 仔细观察`Point `的定义:
879+
880+ ```java
881+ record Point (int x , int y ) {}
882+ ```
883+
884+ 把上述定义改写为class,相当于以下代码:
885+
886+ ```java
887+ final class Point extends Record {
888+ private final int x;
889+ private final int y;
890+
891+ public Point (int x , int y ) {
892+ this . x = x;
893+ this . y = y;
894+ }
895+
896+ public int x () {
897+ return this . x;
898+ }
899+
900+ public int y () {
901+ return this . y;
902+ }
903+
904+ public String toString () {
905+ return String . format(" Point[x=%s, y=%s]" , x, y);
906+ }
907+
908+ public boolean equals (Object o ) {
909+ ...
910+ }
911+ public int hashCode () {
912+ ...
913+ }
914+ }
915+ ```
916+
917+ 除了用`final `修饰class以及每个字段外,编译器还自动为我们创建了构造方法,和字段名同名的方法,以及覆写`toString ()`、`equals ()`和`hashCode ()`方法。
918+
919+ 换句话说,使用`record`关键字,可以一行写出一个不变类。
920+
921+ 和`enum`类似,我们自己不能直接从`Record`派生,只能通过`record`关键字由编译器实现继承。
922+
923+ ### 构造方法
924+
925+ 编译器默认按照`record`声明的变量顺序自动创建一个构造方法,并在方法内给字段赋值。那么问题来了,如果我们要检查参数,应该怎么办?
926+
927+ 假设`Point`类的`x`、`y`不允许负数,我们就得给`Point`的构造方法加上检查逻辑:
928+
929+ ```java
930+ public record Point (int x , int y ) {
931+ public Point {
932+ if (x < 0 || y < 0 ) {
933+ throw new IllegalArgumentException ();
934+ }
935+ }
936+ }
937+ ```
938+
939+ 注意到方法`public Point {... }`被称为Compact Constructor ,它的目的是让我们编写检查逻辑,编译器最终生成的构造方法如下:
940+
941+ ```java
942+ public final class Point extends Record {
943+ public Point (int x , int y ) {
944+ // 这是我们编写的Compact Constructor:
945+ if (x < 0 || y < 0 ) {
946+ throw new IllegalArgumentException ();
947+ }
948+ // 这是编译器继续生成的赋值代码:
949+ this . x = x;
950+ this . y = y;
951+ }
952+ ...
953+ }
954+ ```
955+
956+ 作为`record`的`Point`仍然可以添加静态方法。一种常用的静态方法是`of ()`方法,用来创建`Point`:
957+
958+ ```java
959+ public record Point (int x , int y ) {
960+ public static Point of() {
961+ return new Point (0 , 0 );
962+ }
963+ public static Point of(int x, int y) {
964+ return new Point (x, y);
965+ }
966+ }
967+ ```
968+
969+ 这样我们可以写出更简洁的代码:
970+
971+ ```java
972+ var z = Point.of ();
973+ var p = Point.of (123 , 456 );
974+ ```
975+
976+
977+
0 commit comments