Java中你不知道的5个隐藏秘密

随着编程语言的发展,不可避免地会出现隐藏功能,而创始人从未打算使用的构造开始逐渐普及。这些功能中的一些功能成语,成为语言的公认用语,而其他功能则成为反模式,并降级到语言社区的黑暗角落。在本文中,我们将研究五个Java秘密,这些秘密通常被大量Java开发人员忽略(有些理由很充分)。对于每个描述,我们将研究使每个功能都存在的用例和基本原理,并查看一些适合使用这些功能的示例。
读者应注意,并非所有这些功能并未真正隐藏在语言中,而是经常在日常编程中未使用。尽管有些在适当的时候可能非常有用,但有些几乎总是一个不好的主意,在本文中显示这些是为了引起读者的兴趣(并可能使他或她开心地笑)。读者在决定何时使用本文中描述的功能时应该使用自己的判断:仅仅因为可以做到并不意味着就应该这样做。

1.注释实现

从Java Development Kit(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;
}
}

注意,我们必须实现该 annotationType 方法并返回注释的类型,因为这是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”);

因此,当我们希望初始化一个对象时,我们将初始化逻辑整合到该对象的构造函数中。例如,我们name 在Foo 类的参数化构造函数中设置类的 字段 。尽管似乎可以合理地假设 所有 初始化逻辑都在类的构造函数或构造函数集中找到,但Java并非如此。相反,当创建对象时,我们还可以使用实例初始化来执行代码:
public class Foo {
{
System.out.println(“Foo:instance 1”);
}
public Foo() {
System.out.println(“Foo:constructor”);
}
}

Instance initializers are specified by adding initialization logic within a set of braces within the definition of a class. When the object is instantiated, its instance initializers are called first, followed by its constructors. Note that more than one instance initializer may be specified, in which case, each is called in the order it appears within the class definition. Apart from instance initializers, we can also create static initializers, which are executed when the class is loaded into memory. To create a static initializer, we simply prefix an initializer with the keyword static:
public class Foo {
{
System.out.println(“Foo:instance 1”);
}
static {
System.out.println(“Foo:static 1”);
}
public Foo() {
System.out.println(“Foo:constructor”);
}
}

当所有三种初始化技术(构造函数,实例初始值设定项和静态初始值设定项)都存在于类中时,总是首先以声明它们的顺序执行静态初始值设定项(将类加载到内存时),然后依次执行实例初始值设定项。它们被声明,最后由构造函数声明。引入超类时,执行顺序会稍有变化:
1.超类的静态初始化器,按其声明顺序
2.子类的静态初始化器,按其声明顺序
3.超类的实例初始值设定项,按其声明顺序
4.超类的构造函数
5.子类的实例初始化器,按照其声明顺序
6.子类的构造方法
例如,我们可以创建以下应用程序:
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 myInts = new ArrayList<>();
myInts.add(1);
myInts.add(2);
myInts.add(3);

虽然这完成了我们创建用三个值初始化的新列表的目标,但它过于冗长,要求开发人员为每次添加重复列表变量的名称。为了缩短此代码,我们可以使用双括号初始化来添加相同的三个元素:
List myInts = new ArrayList<>() {{
add(1);
add(2);
add(3);
}};

双括号初始化(从两个大括号和两个大括号的集合中得名)实际上是多个语法元素的组合。首先,我们创建一个匿名内部类来扩展 ArrayList 该类。由于 ArrayList 没有抽象方法,因此我们可以为匿名实现创建一个空的主体:
List myInts = new ArrayList<>() {};

使用此代码,我们实质上创建了一个ArrayList 与原始完全相同的 匿名子类 ArrayList。主要区别之一是,this 由于我们正在创建一个非静态的内部类,因此我们的内部类对包含的类有隐式引用(以捕获的变量的形式 )。这使我们能够编写一些有趣的(如果不是很复杂的话)逻辑,例如将捕获的this 变量添加 到匿名的,双括号初始化的内部类中:
public class Foo {
public List getListWithMeIncluded() {
return new ArrayList() {{
add(Foo.this);
}};
}
public static void main(String… args) {
Foo foo = new Foo();
List fooList = foo.getListWithMeIncluded();
System.out.println(foo.equals(fooList.get(0)));
}
}

如果此内部类是静态定义的,那么我们将无权访问 Foo.this。例如,以下代码(该代码静态创建命名的 FooArrayList 内部类)无法访问该 Foo.this 引用,因此无法编译:
public class Foo {
public List getListWithMeIncluded() {
return new FooArrayList();
}
private static class FooArrayList extends ArrayList {{
add(Foo.this);
}}
}

重新ArrayList创建双括号初始化的构造 ,一旦我们创建了非静态内部类,我们就使用实例初始化(如上所示)在实例化匿名内部类时执行三个初始元素的加法。由于匿名内部类会立即实例化,并且匿名内部类中只有一个对象存在,因此我们实质上创建了一个非静态内部单例对象,该对象在创建时会添加三个初始元素。如果我们分开两个大括号,这将变得更加明显,其中一个大括号清楚地构成了匿名内部类的定义,另一个大括号表示了实例初始化逻辑的开始:
List myInts = new ArrayList<>() {
{
add(1);
add(2);
add(3);
}
};

尽管此技巧很有用,但JDK 9(JEP 269)已用一组静态工厂方法List (以及许多其他收集类型)代替了此技巧的实用程序 。例如,我们可以List 使用这些静态工厂方法创建 以上代码,如以下清单所示:
List myInts = List.of(1, 2, 3);

之所以需要这种静态工厂技术,有两个主要原因:(1)没有创建匿名内部类;(2)减少了创建匿名内部类所需的样板代码(噪音) List。List 以这种方式创建a的警告 是结果 List 是不可变的,因此一旦创建就无法修改。为了创建List 具有所需初始元素的可变项 ,我们坚持使用天真的技术或双括号初始化。
请注意,天真的初始化,双括号初始化和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);

重要的是在决定使用双括号初始化之前要考虑它的性质。虽然确实提高了代码的可读性,但它带有一些隐式的副作用。

4.可执行注释

注释几乎是每个程序的基本组成部分,注释的主要好处是它们不被执行。当我们在程序中注释掉一行代码时,这一点变得更加明显:我们希望将代码保留在我们的应用程序中,但我们不希望它被执行。例如,以下程序导致 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格式的文本文件使用。添加此回车符会将分配推入value = 8注释后的行,以确保其已执行。这意味着以上代码段实际上等于以下代码段:
public static void main(String args[]) {
int value = 5;
//
value = 8;
System.out.println(value);
}

尽管这似乎是Java中的错误,但实际上是该语言中的自觉包含。Java的最初目标是创建独立于平台的语言(因此创建Java虚拟机或JVM),并且源代码的互操作性是此目标的关键方面。通过允许Java源代码包含Unicode字符,我们可以通用方式包含非拉丁字符。这样可以确保在世界一个区域中编写的代码(其中可能包含非拉丁字符,例如在注释中)可以在其他任何地方执行。有关更多信息,请参见Java Language Specification或JLS的3.3节。
我们可以将其发挥到极致,甚至可以使用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();

现在,我们还可以Person 在需要Speaker 对象的任何地方 使用实例 。此外,我们还可以在每个常量的基础上提供接口抽象方法的实现(称为特定于常量的方法):
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();

结论
在本文中,我们研究了Java中的五个隐藏秘密,即:(1)可以扩展注释;(2)实例初始化可用于在实例化时配置对象;(3)双括号初始化可用于执行创建匿名内部类时的说明,(4)有时可以执行注释,(5)枚举可以实现接口。尽管其中一些功能有其适当的用途,但应避免使用其中某些功能(即创建可执行注释)。在决定使用这些机密时,请务必遵循以下规则:仅仅是因为可以做些事情,并不意味着应该这样做。
最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。在这里插入图片描述

发布了76 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/zhaozihao594/article/details/104170540