工学1号馆

home

java高级教程系列1--怎样创建与销毁对象

By Wu Yudong on November 03, 2015

本文主要介绍在java中怎样创建与销毁对象

1. 创建实例

Java是面向对象的语言,创建一个新的类实例(对象)可能是最重要的概念。构造器在创建一个新的类实例的时候扮演着核心的角色,java提供了很多的方法来定义它们。

1.1 隐式(生成)构造函数

Java 允许定义没有任何构造函数的类,但是并不意味着类为空,例如:

public class NoConstructor {
}

这个类没有构造函数,但是Java编译器会自动添加一个,并且新的类实例可以使用 new 关键字.

final NoConstructor noConstructorInstance = new NoConstructor();

1.2 无参构造函数

无参构造函数是明确java编译器工作的最简单的方法.

public class NoArgConstructor {
    public NoArgConstructor() {
        // Constructor body here
    }
}

一旦创建新的类实例,这个构造函数可以使用 new 关键字来被调用.

final NoArgConstructor noArgConstructor = new NoArgConstructor();

1.3 带参数的构造函数

带参数的构造函数是给类实例传递参数的最有意思并且是最有用的方法. 下面的例子定义一个含有两个参数的构造函数.

public class ConstructorWithArguments {
    public ConstructorWithArguments(final String arg1,final String arg2) {
        // Constructor body here
    }
}

在这个例子中,当类实例使用 new 关键字被创建的时候,同时构造函数的参数也必须提供.

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

构造函数之间可以使用 this 关键字互相调用,这种方法被认为是可以减少代码重复。例如,让我们增加另一个只有一个参数的构造函数:

public ConstructorWithArguments(final String arg1) {
    this(arg1, null);
}

1.4 初始化块

Java 有另一种提供叫做初始化块的方法的初始化逻辑. 这种特征很少用到但是最好知道它存在.

public class InitializationBlock {
    {
        // initialization code here
    }
}

以某种方式,初始化块可能被视为匿名的无参数构造函数。特定类可能有多个初始化块和他们所有的顺序将调用代码中定义。例如:

public class InitializationBlocks {
    {
        // initialization code here
    }

    {
        // initialization code here
    }

}

初始化块并不是替代构造函数,有可能和它们一起使用. 但它是非常重要的,初始化块总是在构造函数之前被调用。

public class InitializationBlockAndConstructor {
    {
        // initialization code here
    }
    
    public InitializationBlockAndConstructor() {
    }
}

1.5 构造保障

Java 提供了开发人员依赖的初始化保障,未初始化的实例和类(静态)变量自动初始化为它们的默认值。

Type Default Value
boolean False
byte 0
short 0
int 0
long 0L
char \u0000
float 0.0f
double 0.0d
object reference null

Table 1

让我们看下面的一个简单例子:

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(),

结果如下:

booleanMember = false

byteMember = 0

shortMember = 0

intMember = 0

longMember = 0

charMember = 0

floatMember = 0.0

doubleMember = 0.0

referenceMember = null

1.6  可见性

构造函数受限于Java可见性规则和访问控制修饰符,可以确定其他类可以调用一个特定的构造函数。

Modifier Package Subclass Everyone Else
public accessible accessible accessible
protected accessible accessible not accessible
<no modifier> accessible not accessible not accessible
private not accessible not accessible not accessible

Table 2

1.7  垃圾回收

Java (特别是JVM) 使用自动回收机制. 简而言之,每当创建新对象,自动分配内存。因此,当对象不再被引用,被销毁,内存回收。

Java的垃圾回收机制是分代的,基于大多数对象创建不久就会死亡的假设(在它们被创建后就不再引用,这样的就可以被安全地销毁)。大多数的开发者对象的创建在java中很缓慢并且应该尽可能避免新的对象的创建。事实上, 这并不正确:在Java中创建的对象是相当便宜和快速. 开销大的是那些可能阻塞旧的的类并导致停止一切垃圾收集的生命周期长的对象。

2. 静态初始化器

到目前为止,我们已经讲解了类实例构造和初始化。但是Java还支持类级别的构造器初始化,称为静态初始化器。有非常相似的初始化块,除了增加了static关键字。请注意,每个类加载器执行静态初始化一次。例如:

public class StaticInitializationBlock {
    static {
        // static initialization code here
    }
}

和初始化块类似,你可以在类定义中包含任意多个静态初始化块,并且他们将在第一次出现的时候被执行。例如:

public class StaticInitializationBlocks {
    static {
        // static initialization code here
    }

    static {
        // static initialization code here
    }
}

静态初始化块可以从多个并行线程类触发,,Java运行时保证它将以线程安全的方式将只被执行一次。

3. 构造模式

最近几年很多经典易于理解、广泛使用的应用级构造模式出现在java社区. 我们将介绍最出名的几个: singleton, helpers, factory 和dependency injection.

3.1  Singleton

singleton模式在软件开发人员社区是一个最古老的和有争议的模式。基本上,它的主要思想是确保只有类的一个实例在任何给定的时间被创建。如此简单,不过单例引起很多讨论如何使它正确,特别是线程安全的。单例模式比较原始的版本可能看起来像:

public class NaiveSingleton {
    private static NaiveSingleton instance;
    
    private NaiveSingleton() {        
    }
    
    public static NaiveSingleton getInstance() {
        if( instance == null ) {
            instance = new NaiveSingleton();
        }
        return instance;
    }
}

至少这段代码的一个问题是,如果由多个线程并发调用它可能会产生很多的实例类。正确地设计单例模式的方法之一(但以非延迟的方式)是使用类的static final属性。

public class EagerSingleton {
    private static final EagerSingleton instance = new EagerSingleton();
    
    private EagerSingleton() {        
    }
    
    public static EagerSingleton getInstance() {
        return instance;
    }
}

如果你不想浪费你的资源并希望你的单件模式在它们真正需要时慢吞吞地创建,明确地需要同步,可能导致进入低并发多线程环境中:

public class LazySingleton {
    private static LazySingleton instance;
    
    private LazySingleton() {        
    }
    
    public static synchronized LazySingleton getInstance() {
        if( instance == null ) {
            instance = new LazySingleton();
        }
        
        return instance;
    }
}

如今,单件模式在大多数情况下不被认为是一个不错的选择,主要是因为他们使代码难以测试。依赖注入模式的统治(请见下面的依赖注入部分)也使得单件模式是不必要的。

3.2  Utility/Helper Class(通用/辅助类)

工具或辅助类是由许多Java开发人员使用的相当受欢迎的模式。基本上它代表了non-instantiable类(构造函数声明为私有),可以声明为final和只包含static 方法。例如:

public final class HelperClass {
    private HelperClass() {        
    }
    
    public static void helperMethod1() {
        // Method body here
    }
    
    public static void helperMethod2() {
        // Method body here
    }
}

从经验丰富的软件开发人员的角度来看,这样的辅助常常成为各种不相关的容器的方法,这些方法没有在其他的地方被发现但是应该共享,由其他类使用。这样的设计决策在大多数情况下应避免:它总是可以找到另一种重用所需的功能,保持干净和简洁的代码。

3.3  Factory

Factory模式被证明在软件开发人员的手中是非常有用的技术。因此,从工厂方法到抽象工厂。最简单的工厂模式的例子是返回一个特定类的新实例(工厂方法)的static 方法。例如:

public class Book {
    private Book( final String title) {
    }     
    public static Book newBook( final String title ) { 
        return new Book( title );
    }
}

一个可能会争辩说,介绍newBook工厂方法没有太大的意义,但通常使用这种模式使代码可读性更强。工厂模式的另一个差异是接口或抽象类(抽象工厂)。例如,让我们定义一个工厂接口:

public interface BookFactory {
    Book newBook();
}

两种不同的实现,取决于图书馆类型:

public class Library implements BookFactory {
    @Override
    public Book newBook() {
        return new PaperBook();
    }
}

public class KindleLibrary implements BookFactory {
    @Override
    public Book newBook() {
        return new KindleBook();
    }
}

现在,  Book 类隐藏在 BookFactory 接口的实现后面,依然提供普通的方法创建books.

3.4  Dependency Injection( 依赖注入/控制反转)

依赖注入(也称为控制反转)对于类设计者而言被认为是一个好的实践:如果某些类实例依赖于另一个类的实例,这些依赖项应被提供(注入),它通过构造函数(或setter,策略等等)而不是自己创建的实例。让我们看看下面的例子:

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 );
    }
}

The class Dependant needs an instance of DateFormat and it just creates one by calling DateFormat.getDateInstance() at construction time. The better design would be to use constructor argument to do the same thing:

Dependant需要一个DateFormat实例并且通过调用DateFormat.getDateInstance()创建一个。更好的设计是使用构造函数参数来做同样的事情:

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 );
    }
}

在这种情况下,类的所有依赖项都由外部来提供,它将很容易改变日期格式和编写测试用例。

如果文章对您有帮助,欢迎点击下方按钮打赏作者

Comments

No comments yet.
To verify that you are human, please fill in "七"(required)