java教学笔记之对象的创建与销毁

时间:2021-07-27 04:17:59

本课程的目标是帮你更有效的使用Java。其中讨论了一些高级主题,包括对象的创建、并发、序列化、反射以及其他高级特性。本课程将为你的精通Java的旅程提供指导。

1. 引言

在TIOBE 编程语言排名中,Sun 公司于1995年开发的Java语言是世界上使用最广泛的编程语言之一。作为一种通用编程语言,因为强大的工具包和运行时环境、简单的语法、丰富的平台支持(一次编写,到处运行)以及的异常活跃的社区支持,Java语言对软件开发工程师极具吸引力。

在这一系列的文章中,涵盖了Java相关的高级内容,因此假设读者已具有基本语言知识。这并不是一个完整的参考手册,而是让你的技能更上一层楼的详尽指南。

本课程中包含了大量的代码片段,在有些对方为了做对比,会同时提供Java 7和Java 8的示例。

2. 实例构造

作为一种面向对象语言,对象的创建也许就是Java语言中最重要的概念之一。构造方法是在对象实例初始化过程中具有举足轻重的地位,并且Java提供了多种方式来定义构造方法。

2.1 隐式(产生的)构造方法

Java允许在定义类时不声明任何的构造方法,并这并不代表类没有构造方法。我们看下面类的定义:

?
1
2
3
4
package com.javacodegeeks.advanced.construction;
 
public class NoConstructor {
}

这个类未定义构造方法,但是Java编译器会为其隐式生成一个,从而使我们可以使用new关键字来创建新的对象实例。

?
1
final NoConstructor noConstructorInstance = new NoConstructor();

2.2 无参构造方法

无参构造方法是最简单的通过显式声明来替代Java编译生成构造方法的方式。

?
1
2
3
4
5
6
7
package com.javacodegeeks.advanced.construction;
 
public class NoArgConstructor {
  public NoArgConstructor() {
    // Constructor body here
  }
}

在使用new关键字创建新的对象实例时,上面的构造方法就会被调用。

2.3 有参构造方法

有参构造方法最有意思并且广泛使用,通过指定参数来定制新实例的创建。下面的例子中定义了一个有两个参数的构造方法。

?
1
2
3
4
5
6
7
package com.javacodegeeks.advanced.construction;
 
public class ConstructorWithArguments {
  public ConstructorWithArguments(final String arg1,final String arg2) {
    // Constructor body here
  }
}

这种场景中,当使用new关键字来创建实例时,需要同时提供构造方法上定义的两个参数。

?
1
2
final ConstructorWithArguments constructorWithArguments =
  new ConstructorWithArguments( "arg1", "arg2" );

有趣的是构造方法之间可以通过this关键字互相调用。在实践中,推荐通过使用this把多个构造方法链起来以减少代码重复,并从基础上使对象具有单一的初始化入口。作为示例,下面的代码中定义了只有一个参数的构造方法。

?
1
2
3
public ConstructorWithArguments(final String arg1) {
  this(arg1, null);
}

2.4 初始化代码块

除了构造方法,Java还提供了通过初始化代码块进行初始化的逻辑。这种用法虽然少见,但多了解一些也没害处。

?
1
2
3
4
5
6
7
package com.javacodegeeks.advanced.construction;
 
public class InitializationBlock {
  {
    // initialization code here
  }
}

另一方面,初始化代码块也可被看作是无参的隐式构造方法。在一个具体的类中可以定义多个初始化代码块,在执行的时候按照他们在代码中的位置顺序被调用,如下面的代码所示:

?
1
2
3
4
5
6
7
8
9
10
11
12
package com.javacodegeeks.advanced.construction;
 
public class InitializationBlocks {
  {
    // initialization code here
  }
 
  {
    // initialization code here
  }
 
}

实始化代码块并不是为了取代构造方法,相反它们可以同时出现。但是要记住,初始化代码快会在构造方法调用之前被执行。

?
1
2
3
4
5
6
7
8
9
10
package com.javacodegeeks.advanced.construction;
 
public class InitializationBlockAndConstructor {
  {
    // initialization code here
  }
  
  public InitializationBlockAndConstructor() {
  }
}

2.5 保证构造默认值

Java提供了确定的初始化保证,程序员可以直接使用初始化结果。未初始化的实例以及类变量(static)会自动初始化为相应的默认值。

类型 默认值
boolean False
byte 0
short 0
int 0
long 0L
char \u0000
float 0.0f
double 0.0d
对象引用 null
表 1

我们通过下面的例子来验证上表中的默认值:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.javacodegeeks.advanced.construction;
 
public class InitializationWithDefaults {
  private boolean booleanMember;
  private byte byteMember;
  private short shortMember;
  private int intMember;
  private long longMember;
  private char charMember;
  private float floatMember;
  private double doubleMember;
  private Object referenceMember;
 
  public InitializationWithDefaults() {  
    System.out.println( "booleanMember = " + booleanMember );
    System.out.println( "byteMember = " + byteMember );
    System.out.println( "shortMember = " + shortMember );
    System.out.println( "intMember = " + intMember );
    System.out.println( "longMember = " + longMember );
    System.out.println( "charMember = " +
      Character.codePointAt( new char[] { charMember }, 0 ) );
    System.out.println( "floatMember = " + floatMember );
    System.out.println( "doubleMember = " + doubleMember );
    System.out.println( "referenceMember = " + referenceMember );
  }
}

当使用new关键字实例化对象之后:

final InitializationWithDefaults initializationWithDefaults = new InitializationWithDefaults(),
可从控制台中看到输出结果如下:

?
1
2
3
4
5
6
7
8
9
booleanMember = false
byteMember = 0
shortMember = 0
intMember = 0
longMember = 0
charMember = 0
floatMember = 0.0
doubleMember = 0.0
referenceMember = null

2.6 可见性

构造方法遵从Java的可见性规则,并且可以通过访问控制修饰符决定在其他类中是否能调用该构造方法。

修饰符 包可见性 子类可见性 公开可见性
public 可见 可见 可见
protected 可见 可见 不可见
<无修饰符> 可见 不可见 不可见
private 不可见 不可见 不可见
表2

2.7 垃圾回收

Java(准确的说是JVM)拥有自动的垃圾回收机制。简单来讲,当有新对象创建时,会自动为其分配内在;然后当对象不再被引用后,他们会被自动销毁,相应的内存也会被回收。

Java垃圾回收采用分代回收的机制,并基于"大多数对象生命短暂"的假设(即在对象创建之后很快就不会被再引用,所以可以被安全的销毁)。大多程序员习惯性的认为Java中对象创建的效率很低所以要尽可能避免新对象的创建。事实上,这种认识是不对的。在Java中创建对象的开销是相当低的,并且速度很快。真正代来巨大开销的是不必要的长期存活的对象,因此他们最终会被迁移到老年代,并导致stop-the-world发生。

2.8 对象终结器(Finalizers)

前面我们讲述的都是构造方法和对象初始化相关的主题,但还未提及他们的反面:对象销毁。主要是因为Java使用垃圾回收机制来管理对象的生命周期,所以销毁不必要的对象并释放所需内存就成了垃圾回收的职责了。

不过,Java还是提供了另外一种类似于析构函数的终结器(finalizer)的特性,担任多种资源清理的责任。Finalizer一般被看作是危险的事情(因为它会带来多种副作用和性能问题)。通常并不需要finalizer因此要尽量避免使用它(除了极少见的包含大量本地对象(native objects)的场景)。Java 7中引入的try-with-resources语法和AutoCloseable接口可当作finalizer的替代选择,并可写出如下简洁的代码:

?
1
2
3
try ( final InputStream in = Files.newInputStream( path ) ) {
  // code here
}

3. 静态初始化

上面我们学习了类实例的构造与初始化,除此之外,Java还支持类级别的初始化构造,称作静态初始化。静态初始化与上面介绍的初始化代码块类似,只是多了额外的static关键字修饰。需要注意的是静态初始化只会在类加载时执行一次。示例如下:

与初始化代码块类似,可以在类中定义多个静态初始化块,它们在类中的位置决定在初始化时执行的顺序。示例如下;

?
1
2
3
4
5
6
7
8
9
10
11
package com.javacodegeeks.advanced.construction;
 
public class StaticInitializationBlocks {
  static {
    // static initialization code here
  }
 
  static {
    // static initialization code here
  }
}

因为静态初始化块可以被多个并行执行的线程触发(当类被初始加载时),JVM运行时保证初始化的代码以线程安全的方式只被执行一次。

4. 构造器模式

这些年多种容易理解的构造器(创建者)模式被引入到Java社区。下面我们会学习其中比较流行的几个:单例模式、辅助类模式、工厂模式以及依赖注入(也称为控制反转)。

4.1 单例模式

单例是一种历史悠久却在软件开发社区中饱受争议的模式。单例模式的核心理念是保证在任何时候给定的类只有一个对象被创建。虽然听起来很简单,但人们对如何以正确且线程安全的方式创建对象进行了大量的讨论。下面的代码中展示了简单版本的单例模式实现:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.javacodegeeks.advanced.construction.patterns;
 
public class NaiveSingleton {
  private static NaiveSingleton instance;
  
  private NaiveSingleton() {   
  }
  
  public static NaiveSingleton getInstance() {
    if( instance == null ) {
      instance = new NaiveSingleton();
    }
    
    return instance;
  }
}

上面的代码至少有一个问题:在多线程并发场景中可能会创建出多个对象。一种合理的实现方式(但不能延迟加载)是使用类的static`final`属性。如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
final property of the class.
package com.javacodegeeks.advanced.construction.patterns;
 
public class EagerSingleton {
  private static final EagerSingleton instance = new EagerSingleton();
  
  private EagerSingleton() {   
  }
  
  public static EagerSingleton getInstance() {
    return instance;
  }
}

如果你不想浪费宝贵的资源,希望单例对象只在真正需要的时候才被创建,那么就要使用显式的同步方式,不可这种方法可能会降低多线程环境下的并发性(更多关于Java并发的细节将会在Java进阶9-并发最佳实践中详细介绍)。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.javacodegeeks.advanced.construction.patterns;
 
public class LazySingleton {
  private static LazySingleton instance;
  
  private LazySingleton() {   
  }
  
  public static synchronized LazySingleton getInstance() {
    if( instance == null ) {
      instance = new LazySingleton();
    }
    
    return instance;
  }
}

现在,在很多场景下单例模式不再被认为是一种好的选择,因为他们会使代码不易于测试。另外依赖注入模式的产生也使单例模式变得不再必要。

4.2 工具类/辅助类

工具类/辅助类模式在Java开发者当中相当流行。它的核心理念就是使用不可实例化的类(通过声明private构造方法)、可选的final(更多关于声明final类的细节将会在Java进阶3-类和接口的设计中详细介绍)关键字以及静态方法。示例如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.javacodegeeks.advanced.construction.patterns;
 
public final class HelperClass {
  private HelperClass() {   
  }
  
  public static void helperMethod1() {
    // Method body here
  }
  
  public static void helperMethod2() {
    // Method body here
  }
}

很多经验丰富的开发者认为这种模式会让工具类成为各种不相关方法的容器。因为有些方法没有合适的放置位置却需要被其他类使用,就会被误放入工具类中。在大多数场景中也应该避免这种设计:总会有更好的功能复用的方式,保持代码清晰简洁。

4.3 工厂模式

工厂模式被证明是开发者的极其强大的利器,在Java中有多种实现方式:工厂方法和抽象工厂。最简单的例子就是使用static方法返回特定类的实例(工厂方法),如下:

?
1
2
3
4
5
6
7
8
9
10
package com.javacodegeeks.advanced.construction.patterns;
 
public class Book {
  private Book( final String title) {
  }  
 
  public static Book newBook( final String title ) {
    return new Book( title );
  }
}

虽然使用这种方法能提高代码的可读性,但经常争议的一点是难以给newBook工厂方法赋予更丰富的场景。另外一种实现工厂模式的方法是采用接口或抽象类(抽象工厂)。如下,我们定义一个工厂接口:

?
1
2
3
public interface BookFactory {
  Book newBook();
}

根据图片馆的不同,我们可以有多种不同的newBook实现:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Library implements BookFactory {
  @Override
  public Book newBook() {
    return new PaperBook();
  }
}
 
public class KindleLibrary implements BookFactory {
  @Override
  public Book newBook() {
    return new KindleBook();
  }
}

现在,BookFactory的不同实现屏蔽掉了具体Book的不同,却提供了通用的newBook的方法。

4.4 依赖注入

依赖注入(也称为控制反转)被类设计者认为是一种良好的设计实践:如果一些类实例依赖其他类的实例,那些被依赖的实例应该通过构造方法(或者setter方法、策略等方式)提供(注入),而不应该是由实例自己去创建。先看一下下面的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
package com.javacodegeeks.advanced.construction.patterns;
 
import java.text.DateFormat;
import java.util.Date;
 
public class Dependant {
  private final DateFormat format = DateFormat.getDateInstance();
  
  public String format( final Date date ) {
    return format.format( date );
  }
}

Dependant类需要一个DateFormat类的实例并通过在实例化对象时通过DateFormat.getDateInstance()的方式获得。更好的方式应该通过构造方法的参数来完成同样的事情:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.javacodegeeks.advanced.construction.patterns;
 
import java.text.DateFormat;
import java.util.Date;
 
public class Dependant {
  private final DateFormat format;
  
  public Dependant( final DateFormat format ) {
    this.format = format;
  }
  
  public String format( final Date date ) {
    return format.format( date );
  }
}

在上面的例子中,类实例的所有依赖都由外部提供,这样就很容易调整DateFormat,并易于编写测试代码。