哈Ha! 我向您介绍
贾斯汀·阿尔巴诺 (
Justin Albano)的文章“
Java中的5个隐藏的秘密 ”的翻译。
想要成为Java Jedi吗? 揭示Java的古老秘密。 我们将专注于扩展注释,初始化,注释和枚举接口。
随着编程语言的发展,隐藏功能也开始出现,而创始人从未想过的构造也越来越广泛地用于一般用途。 这些功能中的某些功能在语言中已被普遍接受,而其他功能则进入了语言社区的最黑暗的角落。 在本文中,我们将研究许多Java开发人员经常忽略的五个秘密(公平地说,其中一些有充分的理由)。 我们将考虑使用它们的选项,以及导致每个功能出现的原因,以及一些示例,说明何时建议使用这些功能。
读者应了解,并非所有这些功能实际上都是隐藏的,它们在日常编程中并不经常使用。 其中的一些可能在正确的时间非常有用,而使用其他的几乎总是一个坏主意,并且本文将其显示为吸引读者(并可能使他或她发笑)。 读者还必须决定何时使用本文所述的功能:“可以做到这一点并不意味着需要这样做。”
1.实施注释
从Java开发工具包(JDK)5开始,注释是许多Java应用程序和环境的组成部分。 在绝大多数情况下,注释适用于类,字段,方法等构造。 但是,它们也可以用作已实现的接口。 例如,假设我们具有以下注释定义:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { String name(); }
我们通常将此注释应用于如下所示的方法:
public class MyTestFixure { @Test public void givenFooWhenBarThenBaz() {
然后,我们可以按照
在Java中
创建注释中所述处理该注释。 如果我们还想创建一个允许我们将测试创建为对象的接口,那么我们将不得不创建一个新接口,调用其他接口,而不是Test:
public interface TestInstance { public String getName(); }
接下来,我们可以创建TestInstance对象的实例:
public class FooTestInstance implements TestInstance { @Override public String getName() { return "Foo"; } } TestInstance myTest = new FooTestInstance();
尽管我们的注释和接口几乎完全相同,但是重复非常明显,但似乎没有办法将这两种结构结合起来。 幸运的是,外观具有欺骗性,并且有一种方法可以将这两种结构结合起来:实现注释:
public class FooTest implements Test { @Override public String name() { return "Foo"; } @Override public Class<? extends Annotation> annotationType() { return Test.class; } }
请注意,由于这是Annotation接口的隐式部分,因此我们必须实现注释类型方法并返回注释的类型。 尽管在几乎所有情况下,注释的实现都不是设计的正确解决方案(Java编译器将在实现接口时显示警告),但是在某些情况下(例如在注释驱动的框架中),这可能会很有用。
2.非静态初始化块。
与大多数面向对象的编程语言一样,在Java中,对象是仅使用构造函数创建的(某些例外情况,例如反序列化Java对象)。 即使当我们创建用于创建对象的静态工厂方法时,我们也只是将调用封装在对象的构造函数中以实例化它。 例如:
public class Foo { private final String name; private Foo(String name) { this.name = name; } public static Foo withName(String name) { return new Foo(name); } } Foo foo = Foo.withName("Bar");
因此,当我们要初始化一个对象时,我们在对象的构造函数中结合了初始化逻辑。 例如,我们在Foo类的参数化构造函数中设置其name字段。 尽管假定所有初始化逻辑都在该类的构造函数或构造函数集中似乎是合理的,但在Java中并非如此。 相反,我们可以在创建对象时使用
非静态初始化块执行代码:
public class Foo { { System.out.println("Foo:instance 1"); } public Foo() { System.out.println("Foo:constructor"); } }
通过将初始化逻辑添加到类定义中的一组花括号来指定非静态初始化块。 创建对象时,首先调用非静态初始化块,然后调用对象的构造函数。 请注意,您可以指定多个非静态初始化块,在这种情况下,将按照在类定义中指定的顺序来调用每个块。 除了非静态初始化块之外,我们还可以创建在类加载到内存时执行的静态块。 要创建静态初始化块,我们只需添加static关键字:
public class Foo { { System.out.println("Foo:instance 1"); } static { System.out.println("Foo:static 1"); } public Foo() { System.out.println("Foo:constructor"); } }
当类中存在所有三种初始化方法(构造函数,非静态初始化块和静态初始化块)时,总是始终按声明的顺序执行静态方法(将类加载到内存中时),然后按声明的顺序执行非静态初始化块,并且在他们之后-设计师。 当引入超类时,执行顺序会有所变化:
- 静态超类初始化块,按其声明顺序
- 静态子类初始化块,按其声明顺序
- 非静态超类初始化块,按照声明的顺序
- 超类构造函数
- 非静态子类初始化块,按照声明的顺序
- 子类构造函数
例如,我们可以创建以下应用程序:
public abstract class Bar { private String name; static { System.out.println("Bar:static 1"); } { System.out.println("Bar:instance 1"); } static { System.out.println("Bar:static 2"); } public Bar() { System.out.println("Bar:constructor"); } { System.out.println("Bar:instance 2"); } public Bar(String name) { this.name = name; System.out.println("Bar:name-constructor"); } } public class Foo extends Bar { static { System.out.println("Foo:static 1"); } { System.out.println("Foo:instance 1"); } static { System.out.println("Foo:static 2"); } public Foo() { System.out.println("Foo:constructor"); } public Foo(String name) { super(name); System.out.println("Foo:name-constructor"); } { System.out.println("Foo:instance 2"); } public static void main(String... args) { new Foo(); System.out.println(); new Foo("Baz"); } }
如果执行此代码,则会得到以下输出:
Bar:static 1 Bar:static 2 Foo:static 1 Foo:static 2 Bar:instance 1 Bar:instance 2 Bar:constructor Foo:instance 1 Foo:instance 2 Foo:constructor Bar:instance 1 Bar:instance 2 Bar:name-constructor Foo:instance 1 Foo:instance 2 Foo:name-constructor
请注意,即使创建了两个Foo对象,静态初始化块也只能执行一次。 尽管非统计和静态初始化块可能有用,但应将初始化逻辑放在构造函数中,并且在复杂逻辑需要初始化对象状态的情况下,应使用方法(或静态方法)。
3.双括号初始化
许多编程语言都包含某种语法机制,可在不使用详细模板代码的情况下快速而简短地创建列表或地图(或字典)。 例如,C ++包含
括号初始化 ,如果对象的构造函数支持此功能,开发人员可以使用它来快速创建枚举值列表,甚至初始化整个对象。 不幸的是,在JDK 9之前,没有实现这样的功能(稍后会详细介绍)。 为了简单地创建对象列表,我们将执行以下操作:
List<Integer> myInts = new ArrayList<>(); myInts.add(1); myInts.add(2); myInts.add(3);
尽管这满足了我们创建用三个值初始化的新列表的目标,但它太冗长,需要开发人员为每次添加重复列表变量名称。 为了缩短这段代码,我们可以使用
方括号的双重初始化 :
List < Integer >List<Integer> myInts = new ArrayList<>() {{ add(1); add(2); add(3); }};
双括号初始化实际上是几个语法元素的集合,该初始化从一组两个打开和闭合的花括号中得出其名称。 首先,我们创建
一个扩展ArrayList类
的匿名内部类。 由于ArrayList没有抽象方法,因此我们可以为匿名实现创建一个空主体:
List<Integer> myInts = new ArrayList<>() {};
使用此代码,我们实质上创建了一个匿名子类,ArrayList与原始ArrayList完全相同。 主要区别之一是我们的内部类对包含的类有隐式引用(以此对象捕获的变量的形式),因为 我们创建一个非静态内部类。 这使我们能够编写一些有趣的逻辑,即使不会造成混淆。 例如,将此变量添加到用双括号初始化的匿名内部类中:
public class Foo { public List<Foo> getListWithMeIncluded() { return new ArrayList<Foo>() {{ add(Foo.this); }}; } public static void main(String... args) { Foo foo = new Foo(); List<Foo> fooList = foo.getListWithMeIncluded(); System.out.println(foo.equals(fooList.get(0))); } }
如果将此内部类定义为静态,则我们将无法访问Foo.this。 例如,以下创建静态FooArrayList内部类的代码无权访问Foo.this链接,因此无法编译:
public class Foo { public List<Foo> getListWithMeIncluded() { return new FooArrayList(); } private static class FooArrayList extends ArrayList<Foo> {{ add(Foo.this); }} }
通过创建带有双括号的初始化ArrayList来恢复构造,一旦创建了一个非静态内部类,则如上所述,当实例化一个匿名内部类时,我们将使用非静态初始化块来添加三个初始元素。 当创建一个匿名内部类并且当一个匿名内部类只有一个对象时,可以说我们创建了一个非静态内部对象,该对象在创建时会添加三个初始元素。 如果我们分开一对花括号,就会看到这一点,其中一个花括号代表一个匿名内部类的定义,另一个花括号代表实例初始化逻辑的开始:
List<Integer> myInts = new ArrayList<>() { { add(1); add(2); add(3); } };
尽管此技巧可能有用,但JDK 9(
JEP 269 )已用List的一组静态工厂方法(以及许多其他类型的集合)代替了此技巧的用途。 例如,我们可以使用这些静态工厂方法更早地创建一个List,如下所示:
List<Integer> myInts = List.of(1, 2, 3);
使用此静态工厂技术的主要原因有两个:(1)未创建匿名内部类;(2)减少了创建列表所需的标准代码。 应当记住,在这种情况下,列表的结果保持不变,并且在创建后不能更改。 要创建具有任何初始元素的可变列表文件,我们必须使用常规方法或带有双重初始化括号的方法。
请注意,简单的初始化,双括号和JDK 9静态工厂方法不仅可用于List。 它们可用于Set和Map对象,如以下代码片段所示:
在决定使用双括号之前,必须先了解如何初始化双括号。 这样可以提高代码的可读性,但是可能会出现一些副作用。
4.可执行注释
注释几乎是每个程序的组成部分,注释的主要优点是它们不被执行。 当我们在程序中注释掉一行代码时,这一点变得更加明显:我们希望将代码保存在应用程序中,但不希望其执行。 例如,以下程序将结果显示为“ 5”:
public static void main(String args[]) { int value = 5;
许多人认为注释永远不会执行,但这并非完全正确。 例如,以下代码片段将输出什么?
public static void main(String args[]) { int value = 5;
您可以假设它再次是5,但是如果我们运行上面的代码,我们将在输出中看到8。 出现此“错误”的原因是Unicode字符\ u000d。 此字符实际上是
Unicode回车 ,编译器将Java源代码用作Unicode格式的文本文件。 将其添加到代码后,在注释后的行中将值设置为8,以确保其执行。 这意味着上面的代码片段实际上等于以下内容:
public static void main(String args[]) { int value = 5;
尽管这似乎是一个Java错误,但实际上它是该语言的一个特殊添加的功能。 最初的目标是创建独立于平台的语言(因此创建Java或JVM虚拟机),并且源代码的互操作性是该目标的关键方面。 通过允许Java源代码包含Unicode字符,我们可以通用方式使用非拉丁字符。 这样可以确保在世界一个地区(可能包含非拉丁字符,例如注释)中编写的代码可以在其他任何地方执行。 有关更多信息,请参见
第3.3节Java或JLS语言规范 。
我们可以将其发挥到极致,甚至可以使用Unicode编写整个应用程序。 例如,以下程序做什么(源代码,从
Java派生
:注释中的代码执行?! )?
\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020 \u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079 \u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020 \u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063 \u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028 \u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020 \u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b \u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074 \u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020 \u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b \u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d
如果将上面的代码放入名为Ugly.java的文件中并运行它,则Hello world将被打印在标准输出上。 如果将这些Unicode字符转换为
美国信息交换标准码(ASCII)的字符 ,则会得到以下程序:
public class Ugly { public static void main(String[] args){ System.out.println("Hello w"+"orld"); } }
因此,Unicode字符可以包含在Java源代码中,但是,如果不需要它们,则强烈建议不要使用它们(例如,在注释中包含非拉丁字符)。 如果仍然需要它们,请确保它们不包含会更改源代码预期行为的字符,例如回车符。
5.枚举接口的实现
与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),则定义常量的枚举必须实现这种类型的接口。 有关更多信息
,请参见第38段(第176-9页)
Effective Java,第三版 。
结论
在本文中,我们研究了Java中的五个秘密:(1)可以扩展注释;(2)在创建对象时,可以使用非静态初始化块来配置对象;(3)在创建时,可以使用带有双括号的初始化来执行指令。一个匿名内部类,(4)有时可以执行注释,(5)枚举可以实现接口。 尽管这些功能由某些类型的任务使用,但应避免使用其中某些功能(例如,创建可执行注释)。 在决定使用这些机密时,请务必遵守以下规则:“可以做到这一点并不意味着需要这样做。”