如何创建一个对象(一)
这是一个开篇,无论博客还是论坛,都一直潜水,享受各位作者带来的分享,却一直没有什么回馈。
从我创建博客几年来,只有渺渺两篇,还是当时初学编程时的总结,因为总总原因,这个也未坚持下去。几年来一直想要动笔,却一直找不到什么契机,大概是怯于表达,或觉得积累不够,或怠于坚持,我想归根结底还是太懒。
1 创建对象实例
最近一段时间,突然发现创建对象很有意思,这个虽然简单,但往往容易被人忽视,起码在此之前,我就从未以这种角度去总结过它。
在此之前我们要创建一个简单的商店类(POJO):
#code 1
/**
* 商店
*/
public class Shop {
private int shopId;
private String shopName;
private String address;
public int getShopId() { return shopId;}
public void setShopId(int shopId) { this.shopId = shopId;}
public String getShopName() { return shopName;}
public void setShopName(String shopName) { this.shopName = shopName;}
public String getAddress() { return address;}
public void setAddress(String address) { this.address = address;}
@Override
public String toString() {
return "商店名称:" + shopName + ";商店地址:" + address + "\r\n";
}
}
1.1 构造器(Constructor)
这可以说是最简单最常见的创建对象实例的方式,很多时候,我们都会情不自禁的这么写:
#code 1.1 part 1
//创建Shop的实例
Shop shop = new Shop();
这得益于一个公有的Java类,如果没有显式声明受保护或私有的无参构造方法,就默认一个公有的无参构造。
这个写法本身没有任何问题,当我们需要为字段赋值的时候,我们可以用带参构造的方式,为对象属性赋值:
#code 1.1 part 2
public Shop(String shopName, String address) {
this.shopName = shopName;
this.address = address;
}
public Shop(int shopId, String shopName, String address) {
this.shopId = shopId;
this.shopName = shopName;
this.address = address;
}
这是一个简单的例子,我们平常在编码的时候是否直接使用这种方式去创建对象的同时为属性赋值呢?想象一下吧,当这个Shop类复杂到多至几十个属性,你可能需要写几十个构造重载它才能满足需求。更何况重载也不能解决一切问题,譬如:
#code 1.1 part 3
public Shop(String shopName) {
this.shopName = shopName;
}
public Shop(String address) {
this.address = address;
}
代码(#code 1.1 part 3)编译阶段就无法通过,重载可不认为这是两个构造函数,这时候你的需求永远无法被满足,而且用这样的方式难免会存在代码失去控制难以阅读,也会存在某一个细小的错误难以排查,比如你忘了为某个属性赋值或者颠倒了两个同样类型的属性值。不过我们还有其它的选择,例如选择JavaBean的方式为对象属性赋值,利用每个属性的setter方法:
#code 1.1 part 4
Shop shop = new Shop();
//为shop实例字段赋值
shop.setShopName("XX商店");
shop.setAddress("XX省XX市XX区XX街道555号");
作为一个一直使用.Net的编码者,我一直认为JavaBean的写法是比较繁琐的,同样创建对象,.Net可以通过对象的初始化器完成:
#code 1.1 part 5
//定义Shop类
public class Shop
{
public int ShopId{ get; set; }
public String ShopName{ get; set; }
public String Address{ get; set; }
}
//初始化器
Shop shop = new Shop(){
ShopName="XX商店";
Address="XX省XX市XX区XX街道555号";
};
1.2 反射(Reflector)
当然,上述代码都可以通过反射代替,下面是无参构造的反射实例:
#code 1.2 part 1
try {
Shop shop = (Shop) Class.forName("package.Shop").newInstance();
shop.setShopName("XX商店");
shop.setAddress("XX省XX市XX区XX街道555号");
System.out.printf(shop.toString());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
反射的本质是通过指定方式获取元数据。这里通过找到指定类,调用其无参构造得到该实例。带参构造也是如此:
#code 1.2 part 2
try {
Constructor<?>[] ctors = Class.forName("package.Shop").getConstructors();
Shop shop = null;
for (Constructor ctor : ctors) {
if (ctor.getParameterCount() == 2 &&
ctor.toString().contains("package.Shop(java.lang.String,java.lang.String)")) {
shop = (Shop) ctor.newInstance("XX商店", "XX省XX市XX区XX街道555号");
}
}
if (null != shop) System.out.printf(shop.toString());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
1.3 静态工厂方法(static factory method)
静态工厂方法,顾名思义,一定是静态的,但却不同于设计模式的工厂,这里仅仅是返回一个类实例的静态方法。既然是静态方法,本质上和该类当中的其它静态方法并无任何区别。
我们先通过一段java源码示例简单看下静态工厂方法究竟是什么:
#code 1.3 part 1
// java.lang.Class
@CallerSensitive
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
是不是很熟悉?没错,这就是上述反射(代码#code 1.2 part 1)中使用到的Class.forName()方法,且Class中forName()方法具有多个重载,返回参数类型的实例。再看一段源码示例:
#code 1.3 part 2
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
public static String valueOf(int i) {
return Integer.toString(i);
}
这是java.lang.String中的一段代码,只截取两个方法,在java.lang.String类中,重载了大约9个valueOf()方法,分别是各种参数类型以满足各种需求。这些方法都是返回一个java.lang.String实例。
体会一下,那么静态工厂方法相比较于构造方法有哪些优势?
首先,既然是方法,那么命名就能提高代码的可读性,见名思义,一目了然,相反构造器必须和类名一致。
其次,你不必在每次调用它的时候都创建一个新的对象。你可以使用一个缓存的对象返回,或者一个不可变的对象。这避免了创建重复对象有利于提升程序性能。比如Boolean.valueOf()方法返回了一个Boolean对象的实例,不是true就是false。
另外,只要你愿意,静态工厂方法的返回对象可以是任何类型的对象,比较典型的例如java.util.Collections,这个类显式声明了一个私有构造不能让其实例化,然后提供了多达三十几个静态工厂方法用于返回各种类型。因此,它提供了更多的编码上的灵活性,同样,由于java语言特性的限制,构造器只能返回当前类的对象。
服务提供者框架(Service Provider Framework),组成部分包括服务接口(Service Interface)、提供者注册API(Provider Registration API)、服务访问API(Service Access API)、服务提供者接口(Service Provider Interface)。而静态工厂方法是完成它的基础。例如JDBC,服务接口是Connection,服务提供者接口是Driver,DriverManager类当中registerDriver()方法是服务注册API,getConnection()方法是服务访问API。这些元素就组成了服务提供者框架。
#code 1.3 part 3
//服务接口
public interface Connection{
//...这里定义了例如commit()、close()等一系列接口
}
//服务提供者接口
public interface Driver {
}
//注册提供者和获取服务
public class DriverManager {
public static synchronized void registerDriver(Driver driver){}
public static Connection getConnection(String url,Properties info){}
}
再看个例子,假设现在商店因规模扩张、经营方式改变,现在与多个供应商签订协议,需要向多个供应商要货,且自动完成配给。我们知道,多个供应商和商店是完全独立的系统,它们的要货流程未必一致,那如何将它们对接起来?不管这个场景是否合适,我么现在就尝试使用服务提供者框架来做。
#code 1.3 part 4
//首先是商店需要对外定义协议接口
public interface Service {
/** 要货 */
boolean getGoods();
}
public interface Provider {
/** 获取服务 */
Service newService();
}
//接下来定义一个容器注册供应商
public class ServiceManager {
private ServiceManager() { }
private static final Map<String, Provider> providers = new ConcurrentHashMap<>();
public static void registerProvider(String name, Provider p) {
providers.put(name, p);
}
public static Service newInstance(String name) {
Provider p = providers.get(name);
if (null == p) {
throw new IllegalArgumentException("No provider registered with name:" + name);
}
return p.newService();
}
}
//接下来需要每个供应商来实现这些接口,例如现在有A、B两家供应商
//供应商A - 实现
public class ASupplierService implements Service {
@Override
public boolean getGoods() {
System.out.printf("现在是 A供应商 供货流程...\r\n");
return true;
}
}
public class ASupplierProvider implements Provider {
static {
ServiceManager.registerProvider("A", new ASupplierProvider());
}
@Override
public Service newService() {
return new ASupplierService();
}
}
//供应商B同样需要实现一份
public class BSupplierService implements Service {
@Override
public boolean getGoods() {
System.out.printf("现在是 B供应商 供货流程...\r\n");
return true;
}
}
public class BSupplierProvider implements Provider {
static {
ServiceManager.registerProvider("B", new BSupplierProvider());
}
@Override
public Service newService() {
return new BSupplierService();
}
}
//到现在基本已经完成,接下来我们测试下结果
@Test
public void test() throws ClassNotFoundException{
//注册两家供应商
Class.forName("package.ASupplierProvider");
Class.forName("package.BSupplierProvider");
//向A供应商要货
Service serviceA = ServiceManager.newInstance("A");
serviceA.getGoods();
//向B供应商要货
Service serviceB = ServiceManager.newInstance("B");
serviceB.getGoods();
}
//console输出:
/*
现在是 A供应商 供货流程...
现在是 B供应商 供货流程...
*/
服务提供者框架解耦方式是这种将服务提供方和多个提供者实现之间的解耦。在上面例子中我们动态创建了A、B两个供应商,并调用他们的服务,这时候供应商可以不存在,这些服务需要各个供应商自己实现,这无疑提高代码的灵活性、可伸缩性。
1.4 构建器(Builder)
必须得声明的是,这里的Builder并非Gof中的Builder模式,这里介绍的正如题所述,是创建一个类实例的方法。
我们继续拿Shop类举例,我们创建一个Shop对象的时候,为其属性赋值常用方法就那么几个。我么创建一个个构造函数,为适应不同需求重载多个方法,这种时候你一定很苦恼。或者我们用JavaBean的模式为属性赋值,这个确实也没有什么大的问题,但却有可能使对象处于不一致的状态,因为执行赋值(setter)很可能在多个方法或代码块之中,这时候我们就无法保证其对象状态的一致性了。
很多时候,如代码块(#code 1.1 part 5)所述,.Net可以使用对象的初始化器来完成对象初始化,那么Java又该如何实现类似的方法呢?遗憾的是,这需要我们写更多臃肿的代码来让我们调用的时候更加简洁安全。
#code 1.4 part 1
public interface Build<T> { T build(); }
public class Shop {
private int shopId;
private String shopName;
private String address;
public int getShopId() { return shopId; }
public String getShopName() { return shopName; }
public String getAddress() { return address; }
public Shop(Builder builder) {
this.shopId = builder.shopId;
this.shopName = builder.shopName;
this.address = builder.address;
}
public static class Builder implements Build<Shop> {
private int shopId;
private String shopName;
private String address;
@Override
public Shop build() { return new Shop(this); }
public Builder shopId(Integer shopId) {
this.shopId = shopId;
return this;
}
public Builder shopName(String shopName) {
this.shopName = shopName;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
}
}
//test
@Test
public void test() {
Shop shop = new Shop.Builder()
.shopId(23)
.address("顺天路522号")
.shopName("点点")
.build();
Assert.assertEquals(shop.getShopId(),23);
Assert.assertEquals(shop.getAddress(),"顺天路522号");
Assert.assertEquals(shop.getShopName(),"点点");
}
//console :测试通过
我们要知道的一点是,Builder方式会消耗更多的性能,产生更多额外的代码,但是与此相比,Builder方式带来的更好的编程体验、更好的可维护性、更好的安全性是否值得我们去使用,这是我们日常开发中需要思考的,凡事皆有利弊,通过我们的经验对它做正确的取舍。
第一部分就到此结束,但这个话题还没结束,接下来我会继续带来《如何创建一个对象》剩下的内容。