从 JDK5 开始,Java 开始引入注释功能,今后,注释已成为许多 Java 应用程序和框架的重要组成部分。 在绝大多数状况下,注释将被用于描述语言结构,例如类,字段,方法等,可是在另外一种状况下,能够将注释做为可实现的接口。java
在常规的使用方法中,注释就是注释,接口就是接口。例如,下面的代码为接口 MyInterface 添加了一个注释。编程
@Deprecated interface MyInterface { }
而接口也只能起到接口的做用,以下面的代码,Person 实现了 IPerson 接口,并实现了 getName 方法。数组
interface IPerson { public String getName(); } class Person implements IPerson { @Override public String getName() { return "Foo"; } }
若是按注释方式使用,那么就是注释,若是按接口方式使用,那么就是接口。例如,下面的代码定义了一个 Test 注释。app
@Retention(RetentionPolicy.RUNTIME) @interface Test { String name(); }
Test 注释经过 Retention 注释进行修饰。Retention 注释能够用来修饰其余注释,因此称为元注释,后面的 RetentionPolicy.RUNTIME 参数表示注释不只被保存到 class 文件中,jvm 加载 class 文件以后,仍然存在。这样在程序运行后,仍然能够动态获取注释的信息。框架
Test 自己是一个注释,有一个名为 name 的方法,name 是一个抽象方法,须要在使用注释时指定具体的值,其实 name 至关于 Test 的属性。下面的 Sporter 类使用 Test 注释修改了 run 方法。jvm
class Sporter { @Test(name = "Bill") public void run (){ } }
能够经过反射获取修饰 run 方法的注释信息,例如,name 属性的值,代码以下:编程语言
Sporter sporter = new Sporter(); var annotation = sporter.getClass().getMethod("run").getAnnotations()[0]; var method = annotation.annotationType().getMethod("name"); System.out.println(method.invoke(annotation)); // 输出 Bill
因为 Test 中有 name 方法,因此干脆就利用一下这个 name 方法,直接用类实现它,免得再定义一个相似的接口。代码以下:ide
class Teacher implements Test { @Override public String name() { return "Mike"; } @Override public Class<? extends Annotation> annotationType() { return Test.class; } }
要注意的是,若是要实现一个注释,那么必须实现 annotationType 方法,该方法返回了注释的类型,这里返回了 Test 的 Class 对象。尽管大多数状况下,都不须要实现一个注释,不过在一些状况,如注释驱动的框架内,可能会颇有用。this
在 Java 中,与大多数面向对象编程语言同样,可使用构造方法实例化对象,固然,也有一些例外,例如,Java 对象的反序列化就不须要经过构造方法实例化对象(咱们先不去考虑这些例外)。还有一些实例化对象的方式从表面上看没有使用构造方法,但本质上仍然使用了构造方法。spa
例如,经过静态工厂模式来实例化对象,实际上是将类自己的构造方法声明为 private,这样就不能直接经过类的构造方法实例化对象了,而必须经过类自己的方法来调用这个被声明为 private 的构造方法来实例化对象,因而就有了下面的代码:
class Person { private final String name; private Person(String name) { this.name = name; } public String getName() { return name; } // 静态工厂方法 public static Person withName(String name) { return new Person(name); } } public class InitDemo { public static void main(String[] args){ // 经过静态工厂方法实例化对象 Person person = Person.withName("Bill"); System.out.println(person.getName()); } }
所以,当咱们但愿初始化一个对象时,咱们将初始化逻辑放到对象的构造方法中。 例如,咱们在 Person 类的构造方法中经过参数 name 初始化了 name 成员变量。 尽管彷佛能够合理地假设全部初始化逻辑都在类的一个或多个构造方法中找到。但对于 Java,状况并不是如此。在 Java 中,除了能够在构造方法中初始化对象外,还能够经过代码块来初始化对象。
class Car { // 普通的代码块 { System.out.println("这是在代码块中输出的"); } public Car() { System.out.println("这是在构造方法中输出的"); } } public class InitDemo { public static void main(String[] args){ Car car = new Car(); } }
经过在类的内部定义一堆花括号来完成初始化逻辑,这就是代码块的做用,也能够将代码块称为初始化器。实例化对象时,首先会调用类的初始化器,而后调用类的构造方法。 要注意的是,能够在类中指定多个初始化器,在这种状况下,每一个初始化器将按着定义的顺序调用。
class Car { // 普通的代码块 { System.out.println("这是在第 1 个代码块中输出的"); } // 普通的代码块 { System.out.println("这是在第 2 个代码块中输出的"); } public Car() { System.out.println("这是在构造方法中输出的"); } } public class InitDemo { public static void main(String[] args){ Car car = new Car(); } }
除了普通的代码块(初始化器)外,咱们还能够建立静态代码块(也称为静态初始化器),这些静态初始化器在将类加载到内存时执行。 要建立静态初始化器,咱们只需在普通初始化器前面加 static 关键字便可。
class Car { { System.out.println("这是在普通代码块中输出的"); } static { System.out.println("这是在静态代码块中输出的"); } public Car() { System.out.println("这是在构造方法中输出的"); } } public class InitDemo { public static void main(String[] args){ Car car = new Car(); new Car(); } }
静态初始化器只执行一次,并且是最早执行的代码块。例如,上面的代码中,建立了两个 Car 对象,但静态块只会执行一次,并且是最早执行的,普通代码块和 Car 类的构造方法,在每次建立 Car 实例时都会依次执行。
若是只是代码块或构造方法,并不复杂,但若是构造方法、普通代码块和静态代码块同时出如今类中时就稍微复杂点,在这种状况下,会先执行静态代码块,而后执行普通代码块,最后才执行构造方法。当引入父类时,状况会变得更复杂。父类和子类的静态代码块、普通代码块和构造方法的执行规则以下:
按声明顺序执行父类中全部的静态代码块
按声明顺序执行子类中全部的静态代码块
按声明顺序执行父类中全部的普通代码块
执行父类的构造方法
按声明顺序执行子类中全部的普通代码块
执行子类的构造方法
下面的代码演示了这一执行过程:
class Car { { System.out.println("这是在 Car 普通代码块中输出的"); } static { System.out.println("这是在 Car 静态代码块中输出的"); } public Car() { System.out.println("这是在 Car 构造方法中输出的"); } } class MyCar extends Car { { System.out.println("这是在 MyCar 普通代码块中输出的"); } static { System.out.println("这是在 MyCar 静态代码块中输出的"); } public MyCar() { System.out.println("这是在 MyCar 构造方法中输出的"); } } public class InitDemo { public static void main(String[] args){ new MyCar(); } }
许多编程语言都包含某种语法机制,可使用很是少的代码快速建立列表(数组)和映射(字典)对象。 例如,C ++可使用大括号初始化,这使开发人员能够快速建立枚举值列表,甚至在对象的构造方法支持此功能的状况下初始化整个对象。 不幸的是,在 JDK 9 以前,所以,在 JDK9 以前,咱们仍然须要痛苦而无奈地使用下面的代码建立和初始化列表:
List<Integer> myInts = new ArrayList<>(); myInts.add(1); myInts.add(2); myInts.add(3);
尽管上面的代码能够很好完成咱们的目标:建立包含 3 个整数值的 ArrayList 对象。但代码过于冗长,这要求开发人员每次都要使用变量(myInts)的名字。为了简化这段 diamante,可使用双括号来完成一样的工做。
List<Integer> myInts = new ArrayList<>() {{ add(1); add(2); add(3); }};
双花括号初始化其实是多个语法元素的组合。首先,咱们建立一个扩展 ArrayList 类的匿名内部类。 因为 ArrayList 没有抽象方法,所以咱们能够为匿名类实现建立一个空的实体。
List<Integer> myInts = new ArrayList<>() {};
使用这行代码,实际上建立了原始 ArrayList 彻底相同的 ArrayList 匿名子类。他们的主要区别之一是咱们的内部类对包含的类有隐式引用,咱们正在建立一个非静态内部类。 这使咱们可以编写一些有趣的逻辑(若是不是很复杂的话),例如将捕获的此变量添加到匿名的,双花括号初始化的内部类代码以下:
package black.magic; import java.util.ArrayList; import java.util.List; class InitDemo { public List<InitDemo> getListWithMeIncluded() { return new ArrayList<InitDemo>() {{ add(InitDemo.this); }}; } } public class DoubleBraceInitialization { public static void main(String[] args) { List<Integer> myInts2 = new ArrayList<>() {}; InitDemo demo = new InitDemo(); List<InitDemo> initList = demo.getListWithMeIncluded(); System.out.println(demo.equals(initList.get(0))); } }
若是上面代码中的内部类是静态定义的,则咱们将没法访问 InitDemo.this。 例如,如下代码静态建立了名为 MyArrayList 的内部类,但没法访问 InitDemo.this 引用,所以不可编译:
class InitDemo { public List<InitDemo> getListWithMeIncluded() { return new FooArrayList(); } private static class FooArrayList extends ArrayList<InitDemo> {{ add(InitDemo.this); // 这里会编译出错 }} }
从新建立双花括号初始化的 ArrayList 的构造以后,一旦咱们建立了非静态内部类,就可使用实例初始化(如上所述)来在实例化匿名内部类时执行三个初始元素的加法。 因为匿名内部类会当即实例化,而且匿名内部类中只有一个对象存在,所以咱们实质上建立了一个非静态内部单例对象,该对象在建立时会添加三个初始元素。 若是咱们分开两个大括号,这将变得更加明显,其中一个大括号清楚地构成了匿名内部类的定义,另外一个大括号表示了实例初始化逻辑的开始:
List<Integer> myInts = new ArrayList<>() { { add(1); add(2); add(3); } };
尽管该技巧颇有用,但 JDK 9(JEP 269)已用一组 List(以及许多其余收集类型)的静态工厂方法代替了此技巧的实用程序。 例如,咱们可使用这些静态工厂方法建立上面的列表,代码以下:
List<Integer> myInts = List.of(1, 2, 3);
之因此须要这种静态工厂技术,主要有两个缘由:
(1)不须要建立匿名内部类;
(2)减小了建立列表所需的样板代码(噪音)。
不过以这种方式建立列表的代价是:列表是只读的。也就是说一旦建立后就不能修改。 为了建立可读写的列表,就只能使用前面介绍的双花括号初始化方式或者传统的初始化方式了。
请注意,传统初始化,双花括号初始化和 JDK 9 静态工厂方法不只可用于 List。 它们也可用于 Set 和 Map 对象,如如下代码段所示:
Map<String, Integer> myMap1= new HashMap<>(); myMap1.put("key1", 10); myMap1.put("key2", 15); Map<String, Integer> myMap2 = new HashMap<>() {{ put("Key1", 10); put("Key2", 15); }}; Map<String, Integer> myMap3 = Map.of("key1", 10, "key2", 15);
在使用双花括号方式初始化以前,要考虑它的性质,虽然确实提升了代码的可读性,但它带有一些隐式的反作用。例如,会建立隐式对象。
注释几乎是每一个程序必不可少的组成部分,注释的主要好处是它们不被执行,并且容易让程序变得更可读。 当咱们在程序中注释掉一行代码时,这一点变得更加明显。咱们但愿将代码保留在咱们的应用程序中,但咱们不但愿它被执行。 例如,如下程序致使将 5 打印到标准输出:
public static void main(String args[]) { int value = 5; // value = 8; System.out.println(value); }
尽管不执行注释是一个基本的假设,但这并非彻底正确的。 例如,如下代码片断会将什么打印到标准输出呢?
public static void main(String args[]) { int value = 5; // \u000dvalue = 8; System.out.println(value); }
你们必定猜想是 5,可是若是运行上面的代码,咱们看到在 Console 中输出了 8。 这个看似错误的背后缘由是 Unicode 字符\ u000d。 此字符其实是 Unicode 回车,而且 Java 源代码由编译器做为 Unicode 格式的文本文件使用。 添加此回车符会将“value= 8;”换到注释的下一行(在这一行没有注释,至关于在 value 前面按一下回车键),以确保执行该赋值。 这意味着以上代码段实际上等于如下代码段:
public static void main(String args[]) { int value = 5; // value = 8; System.out.println(value); }
尽管这彷佛是 Java 中的错误,但其实是该语言中的内置的功能。 Java 的最初目标是建立独立于平台的语言(所以建立 Java 虚拟机或 JVM),而且源代码的互操做性是此目标的关键。 容许 Java 源代码包含 Unicode 字符,这就意味着能够经过这种方式包含非拉丁字符。 这样能够确保在世界一个区域中编写的代码(其中可能包含非拉丁字符,例如在注释中)能够在其余任何地方执行。 有关更多信息,请参见 Java 语言规范或 JLS 的 3.3 节。 下面是一个更复杂的案例,你们看看输出什么东东。
package black.magic; public class ExecutableComments { public static void main(String args[]) { int value = 5; // \u000d\u0069\u006e\u0074\u0020\u0041value = 20; // A(65)、i(105)、n(110)、t(116) 空格:32 System.out.println(Avalue); } }
与 Java 中的类相比,枚举的局限性之一是枚举不能从另外一个类或枚举继承。 例如,没法执行如下操做:
public class Speaker { public void speak() { System.out.println("Hi"); } } public enum Person extends Speaker { JOE("Joseph"), JIM("James"); private final String name; private Person(String name) { this.name = name; } } Person.JOE.speak();
可是,我可让枚举实现一个接口,并为其抽象方法提供一个实现,以下所示:
public interface Speaker { public void speak(); } public enum Person implements Speaker { JOE("Joseph"), JIM("James"); private final String name; private Person(String name) { this.name = name; } @Override public void speak() { System.out.println("Hi"); } } Person.JOE.speak();
如今,咱们还能够在须要 Speaker 对象的任何地方使用 Person 的实例。 此外,咱们还能够在每一个常量的基础上提供接口抽象方法的实现(称为特定于常量的方法):
public interface Speaker { public void speak(); } public enum Person implements Speaker { JOE("Joseph") { public void speak() { System.out.println("Hi, my name is Joseph"); } }, JIM("James"){ public void speak() { System.out.println("Hey, what's up?"); } }; private final String name; private Person(String name) { this.name = name; } @Override public void speak() { System.out.println("Hi"); } } Person.JOE.speak();
与本文中的其余一些不一样,应在适当的地方鼓励使用此技术。 例如,若是可使用枚举常量(例如 JOE 或 JIM)代替接口类型(例如 Speaker),则定义该常量的枚举应实现接口类型。
在本文中,咱们研究了 Java 中的五个隐藏秘密:
(1)可扩展的注释;
(2)实例初始化可用于在实例化时配置对象;
(3)用于初始化的双花括号;
(4)可执行的注释;
(5)枚举能够实现接口; 尽管其中一些功能有其适当的用途,但应避免使用其中某些功能(即建立可执行注释)。 在决定使用这些机密时,请确保真的有必要这样作。