《Effective Java》学习笔记 - (1) 使用静态工厂方法代替构造器


前言

对于类而言,为了让客户端获取它本身的一个实例,最传统的方法就是使用构造器,还有一种方法就是可以使用静态工厂方法。这是《Effective Java》这本书的第一节,里面主要介绍一点就是尽量使用静态工厂方法而不是构造器。


使用静态工厂方法代替构造器

1. 优点

1.1 静态工厂方法有名称

这一点可以和单例模式联系起来,单例模式中一般使用了getInstance方法来返回一个对象,这个方法的名字就可以提示我们这是一个单例的对象。又比如下面的例子,当我们遇到一个场景,我们需要通过不同的方法来获取Car对象,就可以在这个类里面提供下面两种方法:

public class Car {

    static HashMap<String, Car> map = new HashMap<>();
    private Car(){
    }

    public static Car getCarByInstanceOrNull(String name){
        return map.getOrDefault(name, null);
    }

    public static Car getCarByNameIfNullWillCreate(String name){
        if(!map.containsKey(name)){
            map.put(name, new Car());
        }
        return map.get(name);
    }
}
public class TestCar {
    public static void main(String[] args) {
        final Car aaa = Car.getCarByInstanceOrNull("aaa");  //获取名字为aaa的
        final Car bbb = Car.getCarByInstanceOrNull("bbb");  //获取名字为bbb的
        System.out.println(aaa + " : " + bbb);
        final Car bbb1 = Car.getCarByNameIfNullWillCreate("bbb");   //获取名字为bbb的,如果不存在就创建
        System.out.println(bbb1);
    }
}

结果截图如下:
在这里插入图片描述

其实通过这个代码就可以很清楚看到,不同的方法getCarByNameIfNullWillCreategetCarByInstanceOrNull都是返回实例对象,但是有着不同的功效,而我们使用构造器是没有这种效果的,并且可以看到在这些方法中,可以很明确看出来我们可以控制对象的创建,比如上面就是控制名字不能相同。

下面还有一个例子,Boolean中的valueOf方法

public static Boolean valueOf(boolean b) {
        return b ? TRUE : FALSE;
    }

这个方法中使用valueOf,我们通过参数名就可以明确知道自己想要做什么,要构建一个false还是true的角色等等。此外,我们都可以在一些类中可以使用静态方法来返回一些特定条件的对象。



1.2 不必每次调用的时候都创建一个对象

这种也很好理解,我们可以提前创建好一些对象,然后调用的时候就直接从方法中返回就可以了。比如spring的对象管理,还有单例模式等等都体现了这个思想。



1.3 可以返回类型的任何子类型的对象

看下面这个例子,还是以上面的为基础,现在新增一个Animal类,然后用Car类继承Animal,在里面我们返回了Car的一个对象:

public class Animal {

    public static Animal getCar(){
        return Car.getCarByNameIfNullWillCreate("aaa");
    }
}

这个例子很明显就可以体现出来返回子类型的特点,这种用法一般都是在接口中,比较适用于基于接口的框架,这种框架中,接口为静态工厂方法提供了自然返回对象,而构造器明显是做不到的。



1.4 所返回的对象的类型可以随着每次的调用而发生变化,取决于参数值

这一点其实和第一点所演示的内容差不多了,你可以传入一个参数,然后静态方法根据这个参数返回你需要的对象,而我们知道构造器是做不到这一点的。还是1.1的例子,对比构造器你就可以发现,如果1.1中不把构造器设置成私有的,那么同一个名字的Animal可能会多次创建;如果设置了私有的,通过静态的方法,那么我们就可以控制同一个名字只能创建一次。比较适合使用的场景就是你的对象有某种约束条件的时候可以这么写。

类似的还有EnumSet没有公共的构造器,只有静态工厂方法。在OpenJDK实现中它们返回两种子类之一的一个实例,具体则取决于底层枚举类型的大小:如果它的元素有64个或者更少,就像大多数枚举类型一样,静态工厂方法就会返回一个RegalarEumSet的实例,用单个long进行支持;如果枚举类型有65个或者更多元素,工厂就会返回JumboEnumSet实例,用一个long数组进行支持。最终结果就是,在以后的版本中,这些返回的类型都可以随意增加或者删除,因为基于接口的返回,所以没有多大影响。用户可以不用关心得到了什么对象,只需要得到对象即可。

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null) {
            throw new ClassCastException(elementType + " not an enum");
        } else {
            return (EnumSet)(universe.length <= 64 ? new RegularEnumSet(elementType, universe) : new JumboEnumSet(elementType, universe));
        }
    }



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

这种静态工厂方法构成了服务提供者框架, 有兴趣可以了解下这个框架。这个框架提供了四个组件

  1. 服务接口(Service Interface):这是提供者实现的
  2. 提供者注册API(Provider Registration API):这是提供者用来注册实现的
  3. 服务访问API(Service Access API):这时客户端用来获取服务的实例,你可以理解成上面例子中的 getCarByInstanceOrNull 方法,这个方法中根据名字返回实例对象
  4. 服务提供者接口(Service Provider Interface):表示产生服务接口实例的工厂对象

其中一个很典型的例子就是JDBC,我们来看看JDBC获取数据库连接的几个关键步骤:

//实现连接的几种方式
public class jdbcTest1 {
    @Test
    //将数据库连接需要的四个基本信息放在配置文件中,对配置文件加载就可以了
    //好处:实现数据与代码分离
    public void test1(){
        //1.读取配置文件中4个基本信息
        ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
        String driverClass = bundle.getString("driverClass");
        String url = bundle.getString("url");
        String user = bundle.getString("user");
        String password = bundle.getString("password");
        System.out.println(driverClass);
        System.out.println(url);
        //2. 加载驱动
        try {
            Class.forName(driverClass);
        //3. 获取连接
            Connection connection = DriverManager.getConnection(url, user, password);
            System.out.println(connection);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }



    }
}

其中最关键的就是 2 和 3 ,当然这两步还可以再详细一点:

//1.实例化Driver
Class clazz = Class.forName(driverName);
//获取实例
Driver driver = (Driver) clazz.newInstance();
             
//2.注册驱动
DriverManager.registerDriver(driver);
//3.获取Connection对象
conn = DriverManager.getConnection(url, user, password);

对于JDBC来说,Connection就是其服务接口的一部分,也是最终我们获取的对象,但是要注意的一点是这个Connection是一个接口来的:public interface Connection extends Wrapper, AutoCloseable{},我们操作数据库要使用这个连接对象,也就是这个服务接口,这个服务接口的一些逻辑是由产商自己写好的。而 Driver 就是服务提供者接口,是用来提供 Connection 的,这一点可以去翻源码,在源码中 DriverManager.registerDriver(driver) 这一句把driver对象放到了一个 registeredDrivers 里面,然后在调用 getConnection 的时候是遍历这个 registeredDrivers ,把里面的driver一个一个取出来然后调用 connect 方法,最终获取到Connection的。所以从上面的流程看下来:DriverManager.registerDriver(driver) 就是提供者注册API,用来注册driver的,这里的注册你可以理解为存起来,而 DriverManager.getConnection 就是服务访问API,可以通过这个方法去访问API。下面就是整个源码过程:

//提供者注册API,注册driver用的
public static void registerDriver(Driver driver) throws SQLException {
      registerDriver(driver, (DriverAction)null);
}

public static void registerDriver(Driver driver, DriverAction da) throws SQLException {
      if (driver != null) {
      	  //把driver存到registeredDrivers里面,就算是注册了
          registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
          println("registerDriver: " + driver);
      } else {
          throw new NullPointerException();
      }
}

//服务访问API,意思就是获取服务接口的API
private static Connection getConnection(String url, Properties info, Class<?> caller) throws SQLException {
     ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
     if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
         callerCL = Thread.currentThread().getContextClassLoader();
     }

     if (url == null) {
         throw new SQLException("The url cannot be null", "08001");
     } else {
         println("DriverManager.getConnection(\"" + url + "\")");
         ensureDriversInitialized();
         SQLException reason = null;
         //遍历所有的提供者注册API
         Iterator var5 = registeredDrivers.iterator();

         while(true) {
             while(var5.hasNext()) {
                 DriverInfo aDriver = (DriverInfo)var5.next();
                 if (isDriverAllowed(aDriver.driver, callerCL)) {
                     try {
                         println("    trying " + aDriver.driver.getClass().getName());
                         //使用服务提供者调用connect方法获取服务接口
                         Connection con = aDriver.driver.connect(url, info);
                         if (con != null) {
                             println("getConnection returning " + aDriver.driver.getClass().getName());
                             return con;
                         }
                     } catch (SQLException var8) {
                         if (reason == null) {
                             reason = var8;
                         }
                     }
                 } else {
                     println("    skipping: " + aDriver.getClass().getName());
                 }
             }

             if (reason != null) {
                 println("getConnection failed: " + reason);
                 throw reason;
             }

             println("getConnection: no suitable driver found for " + url);
             throw new SQLException("No suitable driver found for " + url, "08001");
         }
     }
 }




最终我们终于理清楚这四个接口之间的关系了:
1. 服务接口(Service Interface):也就是Connection,这是提供者实现的,是我们最终的目的
2. 服务提供者接口(Service Provider Interface):这个接口就是用来提供服务者的,那么在上面就体现在Driver,Driver.connect方法可以获取到服务接口Connection
3. 服务访问API(Service Access API):这个就是用来获取服务接口的方法,往往里面是通过调用服务提供者接口来获取服务接口,也就是 DriverManager.getConnection
4. 提供者注册API(Provider Registration API):这就是用来注册提供者的,也就是DriverManager.registerDriver方法,你可以理解为将Driver存起来(JDBC是这样做的)

那么下面了解了这四个关系之后,我们就可以自己写一个服务提供者框架了:
假设现在我需要通过手机发送一条短信,那么这个过程就是首先我需要注册一个服务提供者手机,然后通过一个注册提供者API把手机注册,然后调用服务访问API来访问手机,最终进行发短信,整个过程如下:

//1. 服务者接口
public interface Phone {
    //发送短信
    public void sendMsg();
}

//服务接口实现类
public class PhoneImpl implements Phone {
    @Override
    public void sendMsg() {
        System.out.println("手机发送信息");
    }
}

//2. 服务提供者接口
//手机服务提供者接口
public interface PhoneProvider {
    public Phone getService();
}

public class PhoneProviderImpl implements PhoneProvider{

    static{
        ServiceManager.registProvider("手机", new PhoneProviderImpl());
    }

    @Override
    public Phone getService() {
        return new PhoneImpl();
    }
}

//提供者管理类
//服务提供者注册类
public class ServiceManager {
    private ServiceManager() {
    }

    private static final Map<String, PhoneProvider> registerProvider = new HashMap<>();

    /**
     * 3. 提供者注册API
     */
    public static void registProvider(String name, PhoneProvider phoneProvider){
        registerProvider.put(name, phoneProvider);
    }

    /**
     * 4. 服务访问API, 获取服务类
     */
    public static Phone getPhone(String name){
        if(!registerProvider.containsKey(name)){
            throw new RuntimeException("The required service provider could not be found");
        }
        return registerProvider.get(name).getService();
    }
}

测试:

public class TestService {
    public static void main(String[] args) throws ClassNotFoundException {
    	//注册服务提供者
        Class.forName("com.jianglianghao.One.PhoneProviderImpl");
        //然后直接获取
        final Phone phone = ServiceManager.getPhone("手机");
        //调用接口
        phone.sendMsg();
    }
}

在这里插入图片描述



2. 缺点

2.1 类如果没有公共的或者受保护的构造器,就不能被实例化。

如果是私有的,那么类就不能被直接 new 出来了,这时候建议使用复合而不是继承。



2.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);

总之,静态工厂方法和构造器各有好处,因此切忌第一反应就是提供公有构造器而不是先考虑静态工厂





如有错误,欢迎指出!!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值