java知识点巩固

java基础知识点

1、java中==和equals和hashCode的区别

==是比较两个变量的值是否相等,如果是比较两个基本数据类型,就必须使用==。

如果用在对象上,==比较的是两个对象所在内存的地址是否一致。

例如 String a=new String(“123”); String b=new String(“123”); a==b 结果是false,因为 a b 分别对应的是两个不同的对象,所以未false.

而equals是另一种比较方式。反映的是对象或者变量具体的值,即两个对象里面包含的值。

如果一个对象没有复写equals即使用Object的equals方法,查看源码可以看到就是==比较两个对象是否相等。

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

那为什么上面的a.equals(b)==true呢,因为String类复写了equals方法。我们查看一下源码,发现String的equals方法如果非String类型的,直接比较两个对象在内存中的地址。否则将String的字符一个一个拿出来进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public boolean equals(Object var1) {
if (this == var1) {
return true;
} else {
if (var1 instanceof String) {
String var2 = (String)var1;
int var3 = this.value.length;
if (var3 == var2.value.length) {
char[] var4 = this.value;
char[] var5 = var2.value;

for(int var6 = 0; var3-- != 0; ++var6) {
if (var4[var6] != var5[var6]) {
return false;
}
}

return true;
}
}

return false;
}
}
2、基本数据类型,以及各占的字符数

int 4个字节

long 8个字节

byte 1个字节

short 2个字节

double 8个字节

float 4个字节

char 2个字节

boolen 大家众说分谈,boolen表示true或者false编译后为只需要用0 1表示,所以只需要1位,1bit,1/8字节。

也有人说,虽然0 1只需要1位的空间,但是计算机处理的时候,是以字节为单位。即0为00000000 ,1为00000001,所以boolen占1个字节。

3、int 与 integer的区别

int是java的基本数据类型,占4个字节。integer是int的包装类

int不需要实例化就可以使用,integer需要实例化才可以使用。

int默认是0,integer默认是Null

integer实际是对象的引用,当new Integer的时候,实际上是生成一个指针指向此对象,而Int 则是直接存储数据值的

4、java多态的理解

java的特征,封装,继承,多态。

封装是隐藏java类的内部结构,可以在不影响的情况下,去使用java类的内部结构,同事也保护了数据。对于外界,只需要提供他的方法接口。

继承是为了重用父类代码。

多态,即程序中定义的引用变量所指向的具体类型和方法在编程时并不确定,在程序运行时才能确定,引用变量所调用的方法到底是哪一个类中实现的方法。

多态的实现条件继承,重写,向上转型。

那为什么要向上转型呢。例如我要造车,可以有一个造车厂,返回的是car类。这样需要生产的车继承了car类,就不用再返回各种car,只需要返回他们的父类一个car就行了。

5、String、StringBuilder、StringBuffer区别

String类对象一旦声明则不可以改变;而改变的只是地址,原来的字符串还是存在的,并且产生垃圾。可以看String的源代码,可以发现是被final关键字修饰的。

例如String a=”a”;会创建一个匿名对象 o 并放在方法区常量池中。所以每当,下次不一样的值进行赋值时,就会生成新的匿名对象。遇到一样的值,就会使用上次的值。

另一种赋值方式,使用new关键字。例如String b=new String(“b”);会开辟两块堆内存空间,其中一块会变成垃圾系统回收,而且不能入池。如果要入池,需要手工导入,public String intern();

所以在开发过程中不会使用第二种方式。

String与StringBuffer和StringBuilder主要区别在于String是不可变的对象,每次改变的时候都会生成新的对象,然后再指向引用变量。所以经常改变字符串内容时要就不要用String了,特别是内存中无用对象太多的时候,JVM的GC就开始工作了,速度一会变得很慢。而StringBuffer和StringBuilder对象是能够多次修改,并且不会产生新的未使用对象。其中,StringBuffer是线程安全的,不能异步访问。所以StringBuilder速度要快很多。所以修改字符串内容我们多数是使用StringBuilder。

6、什么是内部类,有何作用

将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。

作用

1.每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,   
2.方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。   
3.方便编写事件驱动程序   
4.方便编写线程代码

7、抽象类和接口的区别

抽象类中被abstact修饰的方法一定被重写,没有被abstact修饰的可以不重写,可以有方法体。默认是Public 或者 Protected

接口的所有方法必须重写,没有方法体。

接口中可以含有 变量和方法。但是要注意,接口中的变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰会报编译错误),而方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、 final等修饰会报编译错误),并且接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。

抽象类只能单继承,接口是可以多实现的。

8、抽象类的意义

当我看到方块类是抽象的,我会很关心它的抽象方法。我知道它的子类一定会重写它,而且,我会去找到抽象类的引用。它一定会有多态性的体现。

9、抽象类和接口的应用场景

interface的应用场合
​ A. 类与类之前需要特定的接口进行协调,而不在乎其如何实现。
​ B. 作为能够实现特定功能的标识存在,也可以是什么接口方法都没有的纯粹标识。
​ C. 需要将一组类视为单一的类,而调用者只通过接口来与这组类发生联系。
​ D. 需要实现特定的多项功能,而这些功能之间可能完全没有任何联系。

abstract class的应用场合
​ 一句话,在既需要统一的接口,又需要实例变量或缺省的方法的情况下,就可以使用它。最常见的有:
​ A. 定义了一组接口,但又不想强迫每个实现类都必须实现所有的接口。可以用abstract class定义一组方法体,甚至可以是空方法体,然后由子类选择自己所感兴趣的方法来覆盖。

​ B. 某些场合下,只靠纯粹的接口不能满足类与类之间的协调,还必需类中表示状态的变量来区别不同的关系。abstract的中介作用可以很好地满足这一点。

​ C. 规范了一组相互协调的方法,其中一些方法是共同的,与状态无关的,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定的状态来实现特定的功能

10、抽象类是否可以没有方法和属性

可以

11、接口的意义

建立在很多的对象类、并且类同时拥有很多的方法(需要实现的功能)。这种情景下,使用接口可以非常明显的感觉到接口的作用。接口更像是一种定义的规范

12、泛型中extends和super的区别

extends 上界通配符。例如List<? extends Car> list中都是Car的子类

super 下界通配符。例如 List<? super Car> list中都是Car 的父类

13、父类的静态方法是否能被子类复写

不可以

14、进程与线程的区别

进程是执行程序的基本单位。线程是进程的一个执行单元。一个程序至少有一个进程,一个进程至少有一个线程。

做个简单的比喻:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-”互斥锁”
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
15、final 、finally、finalize区别
  • final是java关键字。

    被final修饰的类,是无法被继承的,并且,该类的所有成员方法都会被隐式的定位为final方法。

    被final修饰的方法,是无法被子类复写的。

    被final修饰的变量只能被赋值一次,赋值后无法再改变。被final修饰的变量必须初始化赋值。第一种初始化是在声明变量的时候进行赋值,第二种,是在其所在的类的构造函数进行赋值。

    这里String类为什么被final修饰,为什么String是可以改变值呢。

    字符串常量池是java堆内存中一个特殊的存储区域,当我们建立一个String对象时,假设常量池不存在该字符串,则创建一个,若存在则直接引用已经存在的字符串。当我们对String对象值改变的时候,例如 String a=”A”; a=”B” 。a是String对象的一个引用(我们这里所说的String对象其实是指字符串常量),当a=“B”执行时,并不是原本String对象(“A”)发生改变,而是创建一个新的对象(“B”),令a引用它。

  • finally是try / catch语句中处理异常的一部分。并附带着一个语句块,表示无论出现异常都会执行的代码块。经常用来释放资源等。

    但是这里一定会执行的说法是错误的。看下面的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class test {

    public static int test(){
    int i=1;
    i=i/0;
    try {
    return i;
    }finally {
    System.out.println("test over");
    }
    }

    public static void main(String[] args) {
    System.out.println(test());
    }
    }
    1
    2
    3
    4
    test start
    Exception in thread "main" java.lang.ArithmeticException: / by zero
    at bean.test.test(test.java:9)
    at bean.test.main(test.java:18)

    可以看到,finally的内容并没有打印出来。这是因为在try之前,i/0就已经抛出了异常。

    我们将代码修改一下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class test {

    public static int test(){
    int i=1;
    System.out.println("test start");
    try {
    System.out.println("test try");
    System.exit(0);
    return i;
    }finally {
    System.out.println("test over");
    }
    }

    public static void main(String[] args) {
    System.out.println(test());
    }
    }
    1
    2
    test start
    test try

    可以看到依然没有执行finally的内容。原因很简单,程序被终止了。因为调用了System.exit(0)。那我们不调用System.exit(0)呢,如果在线程中,线程被打断了,Interrupted,也不会执行finally的内容。

针对finally这里有一个易错点。下面的代码结果输入时几呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class test {

public static int test(){
int i=1;
System.out.println("test start");
try {
System.out.println("test try");
return i;
}finally {
return 2;
}
}

public static void main(String[] args) {
System.out.println(test());
}
}
1
2
3
test start
test try
2

可以看到原先在try中的return方法,被撤销了。仿佛被屏蔽了,事实上确实如此,因为finally用法特殊,所以会撤销之前的return语句,继续执行最后的finally块中的代码。

  • finalize

    首先,大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。

16、序列化的方式
  • 使用Serializable接口。这是隐式的序列化,会自动序列化所有非static和 transient关键字修饰的成员变量。当一个父类实现序列化,子类自动实现序列化,不需要再显示实现Serializable接口。

  • 使用Externalizable接口,Externalizable继承Serializable,须实现writeExternal()和readExternal()方法,而且只能通过手动进行序列化,并且两个方法是自动调用的,因此,这个序列化过程是可控的,可以自己选择哪些部分序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    public class Blip implements Externalizable{
    private int i ;
    private String s;
    public Blip() {}
    public Blip(String x, int a) {
    System.out.println("Blip (String x, int a)");
    s = x;
    i = a;
    }
    public String toString() {
    return s+i;
    }
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
    // TODO Auto-generated method stub
    System.out.println("Blip.writeExternal");
    out.writeObject(s);
    out.writeInt(i);
    //
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    // TODO Auto-generated method stub
    System.out.println("Blip.readExternal");
    s = (String)in.readObject();
    i = in.readInt();
    }
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
    System.out.println("Constructing objects");
    Blip b = new Blip("A Stirng", 47);
    System.out.println(b);
    ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("F://Demo//file1.txt"));
    System.out.println("保存对象");
    o.writeObject(b);
    o.close();
    //获得对象
    System.out.println("获取对象");
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("F://Demo//file1.txt"));
    System.out.println("Recovering b");
    b = (Blip)in.readObject();
    System.out.println(b);
    }

    }
17、Serializable 和Parcelable 的区别

序列化,表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。

1)在使用内存的时候,Parcelable比Serializable性能高,所以推荐使用Parcelable。

2)Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。

3)Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性在外界有变化的情况下。尽管Serializable效率低点,但此时还是建议使用Serializable 。

4)android上应该尽量采用Parcelable,效率至上,效率远高于Serializable

18、静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?

可以被继承,但无法被重写。static修饰函数/变量时,其实是全局函数/变量,它只是因为java强调对象的要
挂,它与任何类都没有关系。靠这个类的好处就是这个类的成员函数调用static方法不用带类名。

19、静态内部类的设计意图

静态内部类使用场景一般是当外部类需要使用内部类,而内部类无需外部类资源,并且内部类可以单独创建的时候会考虑采用静态内部类的设计。

20、成员内部类、静态内部类、局部内部类和匿名内部类的理解,以及项目中的应用
  • 成员内部类是普通的内部类,成员内部类可以无条件访问外部类的属性和方法(包括private成员和静态成员)

    如果内部类访问外部同名的属性是,this.成员变量需要改为 外部类.this.成员变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     class test1 {
    private int a;
    private static String b;

    class innerClass{
    private int a=22;
    public void getOutClass(){
    System.out.println(test1.this.a);
    System.out.println(b);
    }
    }
    }

    成员内部类是依附外部类而存在的,如果要创建内部类,前提是必须存在外部类。

    内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。

  • 静态内部类

    静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。

  • 局部内部类

    局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。(注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。)

  • 匿名内部类

    匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。下面这段代码是一段Android事件监听代码

    1
    2
    3
    4
    5
    6
    7
    8
    scan_bt.setOnClickListener(new OnClickListener() {

    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub

    }
    });

    匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

    (注意,匿名内部类访问局部变量时,局部变量必须被final修饰,在JDK8中可以不用final修饰,但是如果代码对局部变量重新赋值,是不能通过编译的,因为JDK8中默认变量是final类型,即所谓的effective final,因此还是要遵循final变量不能被修改的特性)

21 、string 转换成 integer的方式及原理
1
2
3
4
public static void main(String[] args) {
String a="22";
System.out.println(Integer.parseInt(a));
}

我们来看一下parseInt方法

1
2
3
4
public static int parseInt(String s) throws NumberFormatException {
return parseInt(s,10);
//可以看到默认基数为10,表示为转换为10进制
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public static int parseInt(String s, int radix)
throws NumberFormatException
{
/*
* WARNING: This method may be invoked early during VM initialization
* before IntegerCache is initialized. Care must be taken to not use
* the valueOf method.
*/

if (s == null) {
throw new NumberFormatException("null");
//NULL抛出异常
}

if (radix < Character.MIN_RADIX) {
//小于最小基数2抛出异常
throw new NumberFormatException("radix " + radix +
" less than Character.MIN_RADIX");
}

if (radix > Character.MAX_RADIX) {
//大于最大基数36
throw new NumberFormatException("radix " + radix +
" greater than Character.MAX_RADIX");
}

int result = 0;
boolean negative = false;//是否为负数
int i = 0, len = s.length();//char字符数组下标和长度
int limit = -Integer.MAX_VALUE;//int 大小限制
int multmin;
int digit;
//判断字符串长度是否大于0否则抛出异常
if (len > 0) {
char firstChar = s.charAt(0);//得到第一个字符
if (firstChar < '0') { // Possible leading "+" or "-" 可能是正号或者负号
if (firstChar == '-') {//是负数
negative = true;//置为负数
limit = Integer.MIN_VALUE;
} else if (firstChar != '+')//都不是就抛出
throw NumberFormatException.forInputString(s);

if (len == 1) // Cannot have lone "+" or "-" 不能仅有 “— 或 +” 抛出异常
throw NumberFormatException.forInputString(s);
i++;//下标自增
}
multmin = limit / radix;
while (i < len) {
// Accumulating negatively avoids surprises near MAX_VALUE
//此方法为确定数字的的十进制值
digit = Character.digit(s.charAt(i++),radix);
//小于0,则为非数值字符串
if (digit < 0) {
throw NumberFormatException.forInputString(s);
}
//result第一次为0,第一次肯定为true
if (result < multmin) {
throw NumberFormatException.forInputString(s);
}
//result乘以基数(10)为得到位置
//例如第一次的result为-1,第二次乘以10后为-10
//下面再-=digit(例如:1)则得到-11
//以此类推
result *= radix;
if (result < limit + digit) {
throw NumberFormatException.forInputString(s);
}
//第一次result为0 -=digit则为负值的该digit
result -= digit;
}
} else {
throw NumberFormatException.forInputString(s);
}
return negative ? result : -result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static int digit(int codePoint, int radix) {
//基数必须再最大和最小基数之间
if (radix < MIN_RADIX || radix > MAX_RADIX) {
return -1;
}
if (codePoint < 128) {
// Optimized for ASCII
int result = -1;
//字符在0-9字符之间
if ('0' <= codePoint && codePoint <= '9') {
result = codePoint - '0';
}
//字符在a-z之间
else if ('a' <= codePoint && codePoint <= 'z') {
result = 10 + (codePoint - 'a');
}
//字符在A-Z之间
else if ('A' <= codePoint && codePoint <= 'Z') {
result = 10 + (codePoint - 'A');
}
//通过判断result和基数大小,输出对应值
//通过我们parseInt对应的基数值为10,
//所以,只能在第一个判断(字符在0-9字符之间)
//中得到result值 否则后续程序会抛出异常
return result < radix ? result : -1;
}
return digitImpl(codePoint, radix);
}
22、常用的设计模式
  • 单例模式

    单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

    • 懒汉单例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class Singleton {
      private Singleton() {}
      private static Singleton single=null;
      //静态工厂方法
      public static Singleton getInstance() {
      if (single == null) {
      single = new Singleton();
      }
      return single;
      }
      }

      Singleton通过将构造方法限定为private避免了类在外部被实例化。在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例。

      给getInstance加同步

      1
      2
      3
      4
      5
      6
      public static synchronized Singleton getInstance() {
      if (single == null) {
      single = new Singleton();
      }
      return single;
      }

      双重检查锁定

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public static Singleton getInstance() {
      if (singleton == null) {
      synchronized (Singleton.class) {
      if (singleton == null) {
      singleton = new Singleton();
      }
      }
      }
      return singleton;
      }
    • 饿汉单利

      1
      2
      3
      4
      5
      6
      7
      8
      9
      //饿汉式单例类.在类初始化时,已经自行实例化 
      public class Singleton1 {
      private Singleton1() {}
      private static final Singleton1 single = new Singleton1();
      //静态工厂方法
      public static Singleton1 getInstance() {
      return single;
      }
      }

      饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

  • 如何选择:如果单件模式实例在系统中经常会被用到,饿汉式是一个不错的选择。反之如果单件模式在系统中会很少用到或者几乎不会用到,那么懒汉式是一个不错的选择。
  • 构造者模式参考 技术之大,在乎你我心中

    该模式其实就是说,一个对象的组成可能有很多其他的对象一起组成的,比如说,一个对象的实现非常复杂,有很多的属性,而这些属性又是其他对象的引用,可能这些对象的引用又包括很多的对象引用。封装这些复杂性,就可以使用建造模式。