您的位置:首页 > 编程语言 > Java开发

浅析java构造函数

2014-11-03 14:57 190 查看
在实际编程中,总觉得java的构造方法存在不少缺陷。

  第一,构成方法缺乏语义性。特别是应用到多态这个特性后,构造方法让人相当疑惑。试想下下面的例子
<pre name="code" class="java">class Shape{
...
Shape(int x,int y,int z){//三角形

}
Shape(int x,int y,int z,int t){//四边形

}
Shape(int x,int y,int z,int t,int q){//五边形

}
...
}


  如果没看注释估计没人懂得,这构造方法想干啥。有时候会想java1.5咋不出个annotation,叫 @construction.如果能这么写就太棒了。 
class Shape{
...
@construction
Triangle(int x,int y,int z){

}
...
}
  第二,对于上面的问题你或许会用继承来解决,不过构造方法在应用到继承特性时,存在着一些潜在的问题。不知道为什么java里头规定调用父类super()构造方法时,一定要写在构造方法的第一行。除了语义上的,我们得先初始化父类的组成部分,然后再初始化子类的组成部分,似乎是挺符合逻辑的,如果把类比作一个模板,那这个角度还说的通。就好像我们盖房子要先打地基一样。但是从继承上讲毫无道理。就好我要造辆宝马,得先造一辆车。但是实际上,这是没有先后顺序的,我们并不需要先造一辆车后再造宝马,而是在通过各个零件组装后,它就已经是一辆车,并且是宝马了。举个例子
class Animal{
String name;
...
Animal(String name){
this.name=name;
saySomething();
}

void saySomething(){
System.out.println("I'm a "+name);
}
...
}
class Cat extend Animal{
String feature;
...
Cat(String name,String feature){
super(name);
this.feature=feature;
}
...
@override
void saySomething(){
System.out.println("I'm a "+name+"and my feature is"+feature);
}
}
  当新建个Cat实例时就出问题了,feature属性还没被初始化就被调用了。构造方法对继承的支持并未屏蔽掉这个问题,当然这种问题可以在声明各各变量时就初始化解决,但是这么做就不能将变量声明成final类型了,如果你写的是一个需要被多线程共享的类,那么这个实例很有可能在你不知道的地方被改动,造成很难以合查的问题。

  事实上,我觉得构造方法唯一的作用就是保证类实例化的原子性。避免线程间在变量初始化时相互修改。

  Cat cat=new Cat();

  cat.setName(name);

  cat.setFeature(feature);

  上面这段代码看起来在语义上还是很明晰的,不过它破坏了cat的原子性,cat可能被别的线程做出不可预知的修改。

  郁闷的是,为了将所有参数在构造方法中进行初始化,在参数很多的情况下,相当难看new Cat(name,arg1,arg2,arg3...)当参数很多的时候,原设计者都很可能忘了哪个参数对应哪个。

  所以不是很建议编码时暴露构造方法的使用,他们总会造成些不必要的麻烦。

  下面是我觉得比较好的处理方式。

  对于之前的Shape类,完全可以将构造方法私有化,对外提供静态的工厂方法获取实例:
class Shape{
...
private Shape(){

}

public static Shape getTriangle(...){
do someting...
}

public static Shape getQuadrangle(...){
do someting
}
...
}
  这么写就一目了然了,调用者很容易的就可以区分出这些方法是干啥的,而且将构造方法封装到方法里,提供了些灵活的应用,你可以在方法中提供一套缓存机制,也可以返回Shape的子类。

  另外对于多参数的构造方法,一直没有找到合适的方法。《Effective Java》这本书中提到了一种处理方案。就是为这个类创建个构造器,然后用这个构造器来初始化这个类。引用下原书的代码:
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;

public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;

public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}

public Builder calories(int val){ calories = val; return this; }

public Builder fat(int val){ fat = val; return this; }

public Builder carbohydrate(int val){ carbohydrate = val; return this; }

public Builder sodium(int val){ sodium = val; return this; }

public NutritionFacts build() {
return new NutritionFacts(this);
}
}

private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
所有的属性值都声明为final,这么做直接就屏蔽了线程篡改属性的隐患。这样我们就可以这么来创建个NutritionFacts的实例:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();

语义上让人一目了然,很明显的区分了必填属性和选填属性,就是实现起来太麻烦,在数据量较大时内存上损耗比较多。感觉在实现具体业务逻辑的时候有些应用场景,不过现在好多框架都搞了JavaBean的映射,这些封装倒意义不大。感觉在写底层的jar给别人调用时,这么写还是个不错的选择。当然具体对构造方法的应用还是根据不同场景决定的,上面说的很多问题是在多线程情况下才会发生的,如果项目不是很复杂,或者能预期到后期不需要做多线程的扩展,没必要实现的那么复杂。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 构造方法