java:effectivejava学习笔记之静态工厂方法的简单理解和使用(代码片段)

JMW1407 JMW1407     2023-02-05     210

关键词:

静态工厂方法

一、什么是静态工厂方法?

对于类而言,在我们需要获取一个实例时,最传统的方法都是通过new新建一个对象,这是jvm通过调用构造函数帮我们实例化出来的对象。而静态工厂方法则是另外一种不通过new来获取一个实例的方法,我们可以通过一个类中公有的静态方法,来返回一个实例。

比如有这样一个People类:

class People
	String name;
	int age;
	int weight;

我们传统的获取实例都是通过new:

People People=new People();

知识点:new 究竟做了什么?

简单来说:当我们使用 new 来构造一个新的类实例时,其实是告诉了 JVM 我需要一个新的实例。JVM
就会自动在内存中开辟一片空间,然后调用构造函数来初始化成员变量,最终把引用返回给调用方。

而静态工厂方法可以在类中添加一个公有静态方法来返回一个实例,还是以上面的People类为例子:

class People
	String name;
	int age;
	int weight;

	public static People getPeople()
		return new People();
	

这样我们就可以通过getPeople这个静态方法来获取一个实例:

People firstPeople=People.getPeople();

虽然通过构造器获取实例是最传统的方法,但是在实际开发中,用静态工厂方法代替构造器也是比较常用的。

 Fragment fragment = MyFragment.newIntance();
    // or 
    Calendar calendar = Calendar.getInstance();
    // or 
    Integer number = Integer.valueOf("3");

二、静态工厂方法的优势

1、 静态工厂方法与构造器不同的第一优势在于,它们有名字

由于语言的特性,Java 的构造函数都是跟类名一样的。这导致的一个问题是构造函数的名称不够灵活,经常不能准确地描述返回值,在有多个重载的构造函数时尤甚,如果参数类型、数目又比较相似的话,那更是很容易出错。

比如,如下的一段代码 :

Date date0 = new Date();
Date date1 = new Date(0L);
Date date2 = new Date("0");
Date date3 = new Date(1,2,1);
Date date4 = new Date(1,2,1,1,1);
Date date5 = new Date(1,2,1,1,1,1);

—— Date 类有很多重载函数,对于开发者来说,假如不是特别熟悉的话,恐怕是需要犹豫一下,才能找到合适的构造函数的。而对于其他的代码阅读者来说,估计更是需要查看文档,才能明白每个参数的含义了。

(当然,Date 类在目前的 Java 版本中,只保留了一个无参和一个有参的构造函数,其他的都已经标记为 @Deprecated 了)

而如果使用静态工厂方法,就可以给方法起更多有意义的名字,比如前面的 valueOf、newInstance、getInstance 等,对于代码的编写和阅读都能够更清晰。

举例

class People
	String name;
	int age;
	int weight;
	public static People getNameWeightPeople(String name,int age)
		People nwp=new People();
		nwp.name=name;
		nwp.weight=weight;
		return nwp;
	
	public static People getNameAgePeople(String name,int age)
		People nap=new People();
		nap.name=name;
		nap.age=age;
		return nap;
	
	public static People getAgePeople(int age)
		People ap=new People();
		ap.age=age;
		return ap;
	
	public static People getWeightPeople(int weight)
		People wp=new People();
		wp.weight=weight;
		return wp;
	

这样我们便能通过静态工厂方法定义不同初始化属性但参数类型相同的方法来获取不同的实例对象了。

2、静态工厂方法不用在每次调用的时候都创建一个新的对象

“如果程序经常请求创建相同的对象,并且创建对象的代价很高,则静态工厂方法能极大地提升程序的性能。”—— 《Effective Java》

如果我们的代码在调用某个类的时候只需要一个实例,但是并不关心这个实例是否是一个新的对象,此时通过静态工厂方法便可以很方便的实现,提升程序性能。

例如加载数据库驱动的时候:

Class.forName("com.mysql.jdbc.Driver");

有时候外部调用者只需要拿到一个实例,而不关心是否是新的实例;又或者我们想对外提供一个单例时 —— 如果使用工厂方法,就可以很容易的在内部控制,防止创建不必要的对象,减少开销。

在实际的场景中,单例的写法也大都是用静态工厂方法来实现的。Hi,我们再来聊一聊Java的单例吧

3、静态工厂方法可以返回原返回类型的任何子类型对象

设计模式中的基本的原则之一——『里氏替换』原则,就是说子类应该能替换父类。

显然,构造方法只能返回确切的自身类型,而静态工厂方法则能够更加灵活,可以根据需要方便地返回任何它的子类型的实例。

这样子看起来貌似有点绕,简单的来说,在构造器中我们只能返回当前构造器所在类的对象,而通过静态工厂方法我们可以任意选择返回类型,因此便可以返回该类的任何子类型。

还是以People类为例子:

Class Person 
    public static Person getInstance()
        return new Person();
        // 这里可以改为 return new Player() / Cooker()
    

Class Player extends Person

Class Cooker extends Person

比如上面这段代码,Person 类的静态工厂方法可以返回 Person 的实例,也可以根据需要返回它的子类 Player 或者 Cooker。(当然,这只是为了演示,在实际的项目中,一个类是不应该依赖于它的子类的。但如果这里的 getInstance () 方法位于其他的类中,就更具有的实际操作意义了)

4、在创建带泛型的实例时,能使代码变得简洁

这条主要是针对带泛型类的繁琐声明而说的,需要重复书写两次泛型参数:

Map<String,Date> map = new HashMap<String,Date>();

不过自从 java7 开始,这种方式已经被优化过了 —— 对于一个已知类型的变量进行赋值时,由于泛型参数是可以被推导出,所以可以在创建实例时省略掉泛型参数。

Map<String,Date> map = new HashMap<>();

所以这个问题实际上已经不存在了。

5、可以有多个参数相同但名称不同的工厂方法

构造函数虽然也可以有多个,但是由于函数名已经被固定,所以就要求参数必须有差异时(类型、数量或者顺序)才能够重载了。

举例来说:

class Child
    int age = 10;
    int weight = 30;
    public Child(int age, int weight) 
        this.age = age;
        this.weight = weight;
    
    public Child(int age) 
        this.age = age;
    

Child 类有 age 和 weight 两个属性,如代码所示,它已经有了两个构造函数:Child(int age, int weight) 和 Child(int age),这时候如果我们想再添加一个指定 wegiht 但不关心 age 的构造函数,一般是这样:

public Child( int weight) 
    this.weight = weight;

↑ 但要把这个构造函数添加到 Child 类中,我们都知道是行不通的,因为 java 的函数签名是忽略参数名称的,所以 Child(int age) 跟 Child(int weight) 会冲突。

这时候,静态工厂方法就可以登场了。

class Child
    int age = 10;
    int weight = 30;
    public static Child newChild(int age, int weight) 
        Child child = new Child();
        child.weight = weight;
        child.age = age;
        return child;
    
    public static Child newChildWithWeight(int weight) 
        Child child = new Child();
        child.weight = weight;
        return child;
    
    public static Child newChildWithAge(int age) 
        Child child = new Child();
        child.age = age;
        return child;
    

其中的 newChildWithWeight 和 newChildWithAge,就是两个参数类型相同的的方法,但是作用不同,如此,就能够满足上面所说的类似Child(int age) 跟 Child(int weight)同时存在的需求。
(另外,这两个函数名字也是自描述的,相对于一成不变的构造函数更能表达自身的含义,这也是上面所说的第一条优势 —— 『它们有名字』)

6、可以减少对外暴露的属性

软件开发中有一条很重要的经验:对外暴露的属性越多,调用者就越容易出错。所以对于类的提供者,一般来说,应该努力减少对外暴露属性,从而降低调用者出错的机会。

考虑一下有如下一个 Player 类:

// Player : Version 1
class Player 
    public static final int TYPE_RUNNER = 1;
    public static final int TYPE_SWIMMER = 2;
    public static final int TYPE_RACER = 3;
    protected int type;
    public Player(int type) 
        this.type = type;
    

Player 对外提供了一个构造方法,让使用者传入一个 type 来表示类型。那么这个类期望的调用方式就是这样的:

  Player player1 = new Player(Player.TYPE_RUNNER);
  Player player2 = new Player(Player.TYPE_SWEIMMER);

但是,我们知道,提供者是无法控制调用方的行为的,实际中调用方式可能是这样的:

  Player player3 = new Player(0);
    Player player4 = new Player(-1);
    Player player5 = new Player(10086);

提供者期望的构造函数传入的值是事先定义好的几个常量之一,但如果不是,就很容易导致程序错误。

—— 要避免这种错误,使用枚举来代替常量值是常见的方法之一,当然如果不想用枚举的话,使用我们今天所说的主角静态工厂方法也是一个很好的办法。

插一句:
实际上,使用枚举也有一些缺点,比如增大了调用方的成本;如果枚举类成员增加,会导致一些需要完备覆盖所有枚举的调用场景出错等。
如果把以上需求用静态工厂方法来实现,代码大致是这样的:

// Player : Version 2
class Player 
    public static final int TYPE_RUNNER = 1;
    public static final int TYPE_SWIMMER = 2;
    public static final int TYPE_RACER = 3;
    int type;

    private Player(int type) 
        this.type = type;
    

    public static Player newRunner() 
        return new Player(TYPE_RUNNER);
    
    public static Player newSwimmer() 
        return new Player(TYPE_SWIMMER);
    
    public static Player newRacer() 
        return new Player(TYPE_RACER);
    

注意其中的构造方法被声明为了 private,这样可以防止它被外部调用,于是调用方在使用 Player 实例的时候,基本上就必须通过 newRunner、newSwimmer、newRacer 这几个静态工厂方法来创建,调用方无须知道也无须指定 type 值 —— 这样就能把 type 的赋值的范围控制住,防止前面所说的异常值的情况。

插一句:

严谨一些的话,通过反射仍能够绕过静态工厂方法直接调用构造函数,甚至直接修改一个已创建的 Player 实例的 type值,但本文暂时不讨论这种非常规情况。

7、多了一层控制,方便统一修改

我们在开发中一定遇到过很多次这样的场景:在写一个界面时,服务端的数据还没准备好,这时候我们经常就需要自己在客户端编写一个测试的数据,来进行界面的测试,像这样:

  // 创建一个测试数据
    User tester = new User();
    tester.setName("隔壁老张");
    tester.setAge(16);
    tester.setDescription("我住隔壁我姓张!");
    // use tester
    bindUI(tester);
    ……

要写一连串的测试代码,如果需要测试的界面有多个,那么这一连串的代码可能还会被复制多次到项目的多个位置。

这种写法的缺点呢,首先是代码臃肿、混乱;其次是万一上线的时候漏掉了某一处,忘记修改,那就可以说是灾难了……

但是如果你像我一样,习惯了用静态工厂方法代替构造器的话,则会很自然地这么写,先在 User 中定义一个 newTestInstance 方法:

static class User
    String name ;
    int age ;
    String description;
    public static User newTestInstance() 
        User tester = new User();
        tester.setName("隔壁老张");
        tester.setAge(16);
        tester.setDescription("我住隔壁我姓张!");
        return tester;
    

然后调用的地方就可以这样写了:

 // 创建一个测试数据
    User tester = User.newTestInstance();
    // use tester
    bindUI(tester);

是不是瞬间就觉得优雅了很多?!

而且不只是代码简洁优雅,由于所有测试实例的创建都是在这一个地方,所以在需要正式数据的时候,也只需把这个方法随意删除或者修改一下,所有调用者都会编译不通过,彻底杜绝了由于疏忽导致线上还有测试代码的情况。

8、方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在

静态工厂方法是提供一个Pet实例,可能在在最初的时候,什么品种也没有,但后来新增了几个品种,此时你的方法就可以返回这些新的品种。

public class Pet

	// 存放品种
    private static final Map<String, Pet> map = new HashMap<>();

    // 根据名称获取宠物
    public static Pet getPet(String name) 
        return Optional.ofNullable(map.get(name)).orElseThrow(() -> new IllegalArgumentException("还没这个品种"));
    

    // 后续增加品种,将其放入map
    public static void add (String name, Pet pet) 
        map.put(name, pet);
    

    public static void main(String[] args) 
        try 
            getPet("Cat");
        catch (Exception e) 
        	// 首次获取时由于map里没有Cat,会报错
            System.out.println(e.getMessage());
        
        // 动态进行品种的添加
        add("Cat", new Cat());
        // 再次获取则可以得到猫的实例
        System.out.println(getPet("Cat"));
    



三、静态工厂方法的缺点

  • 1、类如果不含public或protected构造器,就不能被子类实例化

在使用静态工厂方法时,由于经常会处于保护目的,将构造器私有化,由于子类实例化时需要调用父类的构造器,所以私有的构造器会导致类无法被继承。处理的办法是,用复合(composition)取代继承,以实现类的正常拓展。

  • 2、程序员很难发现静态工厂方法

因为静态工厂方法是自定义的,它们没有像构造器那样被明确规定如何实例化,因此程序员往往很难查明如何通过静态工厂方法实例化一个类,因此我们便需要遵守一些静态工厂方法的惯用名称。

以下列出一小部分:

①from——类型转换方法,只有单个参数。返回该类型的一个相对应实例,例如:

Date d=Date.from(instant)

②of——聚合方法,有多个参数,返回该类型的一个实例,把它们合并起来,例如:

Set<Rank> facecards=Enumset.of(JACK, QUEEN, KING);

③valueOf——比from和of更烦琐的一种替代方法,例如:

BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

④ instance或者getInstance——返回的实例是通过方法的(如有)参数来描述的,但是不能说与参数具有同样的值,例如:

Stackwalker luke -Stackwalker.getInstance(options);

⑤create或者 newInstance——像 instance或者 getInstance一样,但 create或者 newInstance能够确保每次调用都返回一个新的实例,例如:

Object newArray=Array.newInstance(classObject,arrayLen);

⑥getType——像 getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型,例如:

FileStore fs=Files.getFileStore(path);

⑦newtype——像newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型,例如:

BufferedReader br=Files.newBufferedReader(path);

⑧type——getType和 newType的简版,例如:

List<Complaint> litany=Collections.list(legacyLitany);

四、总结

总体来说,我觉得『考虑使用静态工厂方法代替构造器』这点,除了有名字、可以用子类等这些语法层面上的优势之外,更多的是在工程学上的意义,我觉得它实质上的最主要作用是:能够增大类的提供者对自己所提供的类的控制力。

作为一个开发者,当我们作为调用方,使用别人提供的类时,如果要使用 new 关键字来为其创建一个类实例,如果对类不是特别熟悉,那么一定是要特别慎重的 —— new 实在是太好用了,以致于它经常被滥用,随时随地的 new 是有很大风险的,除了可能导致性能、内存方面的问题外,也经常会使得代码结构变得混乱。

而当我们在作为类的提供方时,无法控制调用者的具体行为,但是我们可以尝试使用一些方法来增大自己对类的控制力,减少调用方犯错误的机会,这也是对代码更负责的具体体现。

参考

1、Java 的静态工厂方法
2、java静态工厂方法详细解析——使用静态工厂方法代替构造器
3、Effective Java——第一条:用静态工厂方法代替构造器

effectivejava学习笔记之不可实例化的类

在没有显式声明一个类的构造方法时,编译器会生成默认的无参构造方法,在设计工具类时,我们通常将方法设置成静态方法,以类名.方法名的形式调用,此时这个类就没有必要创建实例,我们知道抽象类不可以被实例化,但... 查看详情

java:java学习笔记之java单例模式的简单理解和使用(代码片段)

...单例的实现如下:2、懒汉式终极版本:volatile3、EffectiveJava1——静态内部类4、5.2EffectiveJava2——枚举参考Java单例模式1、饿汉式单例的实现如下://饿汉式实现publicclassSingleBprivatestaticfinalSingleBINSTA 查看详情

java:effectivejava学习笔记之始终要覆盖tostring方法(代码片段)

Java始终要覆盖toString方法始终要覆盖toString方法1、为什么要覆盖toString方法?2、始终要覆盖toString方法始终要覆盖toString方法1、为什么要覆盖toString方法?publicclassPerson protectedStringname; protectedintage; publicStringgetName() retur... 查看详情

java:effectivejava学习笔记之接口只用于定义类型类层次优于标签类(代码片段)

Java接口只用于定义类型1、接口只用于定义类型1.1、常量接口2、类层次优于标签类1、接口只用于定义类型当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明可以对这个类的实... 查看详情

java:effectivejava学习笔记之避免创建不必要的对象(代码片段)

Java避免创建不必要的对象避免创建不必要的对象1、采用更合适的API或工具类减少对象的创建2、重用相同功能的对象3、小心自动装箱(autoboxing)4、用静态工厂方法而不是构造器5、正则表达式6、补参考避免创建不必要的... 查看详情

java:effectivejava学习笔记之通过私有构造器强化不可实例化的能力(代码片段)

Java通过私有构造器强化不可实例化的能力通过私有构造器强化不可实例化的能力参考通过私有构造器强化不可实例化的能力并非所有的类都是需要实例化的。有时候,我们可能需要编写至包含静态方法和静态域的类。这些类... 查看详情

java:effectivejava学习笔记之优先考虑泛型和泛型方法(代码片段)

Java优先考虑泛型和泛型方法1、优先考虑泛型2、优先考虑泛型方法参考1、优先考虑泛型下面我们举个例子,将他作为泛型化的主要备选对象,换句话说,可以适当的强化这个类来利用泛型。publicclassStackprivateObject[]elem... 查看详情

effectivejava学习笔记之所有对象都通用的方法

一、覆盖equals时请遵守通用约定1、满足下列任何一个条件时,不需要覆盖equals方法a、类的每个实例本质上都是唯一的。此时就是Object中equals方法所表达的含义。b、不关心类是否提供了“逻辑相等”的测试功能c、超类中覆... 查看详情

java:effectivejava学习笔记之避免使用终结方法(代码片段)

Java避免使用终结方法避免使用终结方法1、finalize()基本概念2、finalize()的执行过程3、为什么要避免覆盖并使用finalize方法?4、如果类中的资源确实需要被释放,我们应该怎么做?5、终结方法的利弊5.1、终结方法的好... 查看详情

java:effectivejava学习笔记之考虑实现comparable接口(代码片段)

Java考虑实现Comparable接口考虑实现Comparable接口1、Comparable接口2、为什么要考虑实现Comparable接口3、compareTo方法的通用约定4、何时以及如何实现Comparable接口4.1、多重比较5、实现Comparable接口所需满足的需求6、总结参考考虑实现Compa... 查看详情

java:effectivejava学习笔记之覆盖equals时请遵守通用约定(代码片段)

Java覆盖equals时请遵守通用约定覆盖equals时请遵守通用约定1、为什么要覆盖equals2、需要覆盖equals方法的时机2.1、不需要覆盖equals方法的情况2.2、需要覆盖equals方法的情况2.2.1、自反性2.2.2、对称性2.2.3、传递性2.2.4、一致性2.2.5、... 查看详情

java:effectivejava学习笔记之列表优先于数组(代码片段)

Java列表优先于数组列表优先于数组1、协变与不可变类型2、运行时检验与编译器检验3、可具体化与不可具体化4、无法很好混用列表和数组5、案例分析参考列表优先于数组1、协变与不可变类型1、数组是协变类型,指继承关... 查看详情

java:effectivejava学习笔记之请不要在新代码中使用原生态类型(代码片段)

Java请不要在新代码中使用原生态类型1、请不要在新代码中使用原生态类型参考1、请不要在新代码中使用原生态类型1、在没有泛型之前,从集合中读取到每一个对象都必须进行转换。如果有人不小心插入了类型错误的对象&#x... 查看详情

java:effectivejava学习笔记之消除过期对象引用(代码片段)

Java消除过期对象引用消除过期对象引用1、Java的垃圾回收机制2、Java中的内存泄露3、常见的内存泄露参考消除过期对象引用很多人可能在想这么一个问题:Java有垃圾回收机制,那么还存在内存泄露吗?答案是肯定的&#... 查看详情

java:effectivejava学习笔记之用私有构造器或者枚举类型强化singleton属性(代码片段)

Java强化SIngleton属性强化SIngleton属性1、Singleton模式2、静态成员方式3、用私有构造器来强化4、使用枚举实现单例模式参考强化SIngleton属性1、Singleton模式Hi,我们再来聊一聊Java的单例吧在Java1.5发版之前,实现Singleton有两种... 查看详情

java:effectivejava学习笔记之接口优于抽象类(代码片段)

Java接口优于抽象类接口优于抽象类1、接口和抽象类2、接口优点3、骨架类3.1、demo参考接口优于抽象类1、接口和抽象类Java中抽象类和接口的区别2、接口优点1、现有的类可以很容易的被更新,以实现新的接口。如果你前期编... 查看详情

java《effectivejava中文版第2版》学习笔记遇到多个构造器时要考虑用构建器

  静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。  当一个类中有若干个必选属性和多个可选属性时,采用重叠构造器模式、JavaBeans模式或者Builder模式,但各有优劣。  当有很多参数的... 查看详情

java:effectivejava学习笔记之覆盖equals时总要覆盖hashcode(代码片段)

Java覆盖equals时总要覆盖hashcode覆盖equals时总要覆盖hashcode1.什么是hashcode方法?2.hashcode相等与对象相等之间的关系:(保证设计是规范的前提下)3.为什么要覆盖hashcode3.1、覆盖equals时总要覆盖hashCode3.2、如何在覆盖... 查看详情