译文
随着编程语言的发展,隐藏的功能开始出现是不可避免的,创始人从未想过的结构(constructs)开始蔓延到普通用途。(,不好翻译,总体意思就是有些功能本来是一种功能,但是在其他用途方面反而发挥作用)其中一些特征逐渐成为人们的习惯,并成为语言中公认的用语,而其他的一些特性则相反,被降级到语言社区的黑暗角落。在本文中,我们将看看大量Java开发人员经常忽略的五个Java秘密。通过这些描述,我们将查看每个功能的用例和基本原理,并在可能适合使用这些功能时查看一些示例。
读者应该注意,并非所有这些功能都没有真正隐藏在语言中,只是在日常节目中经常没用。在决定何时使用本文中描述的功能时,读者应该使用自己的判断:仅仅因为它可以完成并不意味着它应该(比较拗口,应该说这是隐藏的第二功能,而并非主要功能)。
注解 Implementation
自Java Development Kit(JDK)5以来,注解都是许多Java应用程序和框架的组成部分。在绝大多数情况下,注解应用于语言结构,例如,类,变量,方法等。但是还有另一种情况可以应用注解:作为可继承(implementable )的接口。例如,假设我们有以下注解定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
String name();
}
通常,我们会将此注解应用于方法,如下所示:
public class MyTestFixure {
@Test
public void givenFooWhenBarThenBaz() {
// ...
}
}
如果我们还想创建一个允许将测试创建为对象的接口,我们必须创建一个新接口,将其命名为Test以外的其他接口:
public interface TestInstance {
public String getName();
}
然后我们可以实例化一个TestInstance对象:
public class FooTestInstance implements TestInstance {
@Override
public String getName() {
return "Foo";
}
}
TestInstance myTest = new FooTestInstance();
虽然我们的注解和接口几乎相同,有非常明显的重复,似乎没有办法合并这两个结构。幸运的是,外观具有欺骗性的,存在一种合并这两种结构的技术:注解implements:
public class FooTest implements Test {
@Override
public String name() {
return "Foo";
}
@Override
public Class<? extends Annotation> annotationType() {
return Test.class;
}
}
请注意,我们必须实现annotationType方法并返回注解的类型,这是因为注解接口的隐式部分。虽然几乎在所有情况下,实现注解都不是一个合理的设计决策(Java编译器在实现接口时会显示警告),但是它可以在少数情况下使用,例如在注解驱动的框架内。
实例初始化
在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:(简单来说 执行顺序:Static初始化 < 实例初始化 <构造器初始化)
public class Foo {
{
System.out.println("Foo:instance 1");
}
static {
System.out.println("Foo:static 1");
}
public Foo() {
System.out.println("Foo:constructor");
}
}
当三个初始化技术都出现在一起的时候,始终首先执行静态初始化(当类加载到内存中时),然后按实例初始化程序声明它们的顺序执行,最后则是构造器。引入超类时,执行顺序会略有变化:
- 父类的静态初始化
- 子类的静态初始化
- 父类的实例初始化
- 父类的构造器
- 子类的实例初始化
- 子类的构造器
请注意,即使创建了两个Foo对象,静态初始化程序也只执行一次。虽然实例和静态初始化器可能很有用,但初始化逻辑应放在构造函数中,并且当需要复杂逻辑来初始化对象的状态时,应使用方法(或静态方法)。
双括号初始化
许多编程语言都包含一些语法机制,可以快速简洁地创建列表或Map(或字典),而无需使用详细的样板代码。C ++包括大括号初始化,允许开发人员快速创建枚举值列表,甚至在对象的构造函数支持此功能时初始化整个对象。不幸的是,在JDK 9之前,没有包含这样的功能(我们将很快介绍这个功能)。为了简单地创建对象列表,我们将执行以下操作:
List<Integer> myInts = new ArrayList<>();
myInts.add(1);
myInts.add(2);
myInts.add(3);
虽然这实现了我们用三个值初始化新列表的目标,但它过于冗长,要求开发人员为每次添加重复列表变量的名称。为了缩短这段代码,我们可以使用双括号初始化来添加相同的三个元素:
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);
(ps:包那么多语法糖有啥意思,并不能提高生产力)
他的静态工厂技术是可取的,主要有两个原因:(1)没有创建匿名内部类,(2)创建List所需的样板代码减少。(忘掉我上面所说的)
以这种方式创建生成的List是不可变的,因此一旦创建它就无法修改。为了创建一个带有所需初始元素的可变List,我们坚持使用naive技术或双括号初始化。
请注意,初始化初始化,双括号初始化和JDK 9静态工厂方法不仅适用于List。它们也可用于Set和Map对象,如以下代码段所示:
// Naive initialization
Map<String, Integer> myMap = new HashMap<>();
myMap.put("Foo", 10);
myMap.put("Bar", 15);
// Double-brace initialization
Map<String, Integer> myMap = new HashMap<>() {{
put("Foo", 10);
put("Bar", 15);
}};
// Static factory initialization
Map<String, Integer> myMap = Map.of("Foo", 10, "Bar", 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,但如果我们运行上面的代码,我们看到8打印到标准输出。这个看似错误背后的原因是Unicode字符\ u000d;这个字符实际上是一个Unicode回车符,Java源代码被编译器用作Unicode格式的文本文件。添加此回车符将分配值= 8推送到注释后面的行,确保它已执行。这意味着上面的代码片段实际上等于以下内容:
public static void main(String args[]) {
int value = 5;
//
value = 8;
System.out.println(value);
}
虽然这似乎是Java中的一个错误,但它实际上是有意识地包含在语言中。ava的最初目标是创建独立于平台的语言(因此创建Java虚拟机或JVM),源代码的互操作性是此目标的关键方面。通过允许Java源代码包含Unicode字符,我们可以以通用方式包含非拉丁字符。这确保了在世界的一个区域中编写的代码(可以包括非拉丁字符,例如在注释中)可以在任何其他区域中执行。
我们可以将其发挥到极致,甚至用Unicode编写整个应用程序。例如,以下程序执行什么操作?
\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");}}
虽然知道Java源代码中可以包含Unicode字符很重要,但强烈建议除非需要(例如,在注释中包含非拉丁字符),否则如果需要它们,请确保不要包含更改源代码的预期行为的字符,例如回车符。避免使用它们。
枚举接口实现
与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),则定义常量的枚举应实现接口类型。
PS:个人比较讨厌语法糖很多的语言,适当适量的语法糖可以提高生产力,简化开发。而过度使用语法糖,只会让代码维护困难。