面向对象
封装
数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
减少耦合,提高可维护性可重用性
继承
继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系
多态
多态分为编译时多态和运行时多态:
- 编译时多态主要指方法的重载
- 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
运行时多态有三个条件:
- 继承
- 覆盖(重写)
- 向上转型
重写与重载
1. 重写(Override)
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
为了满足里式替换原则,重写有以下两个限制:
- 子类方法的访问权限必须大于等于父类方法;
- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
使用 @Override 注解,可以让编译器帮忙检查是否满足上面的两个限制条件。
2. 重载(Overload)
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
应该注意的是,返回值不同,其它都相同不算是重载。
类图
实现关系 (Realization)
用来实现一个接口,在 Java 中使用 implements 关键字。
聚合关系 (Aggregation)
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。
组合关系 (Composition)
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
关联关系 (Association)
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。
依赖关系 (Dependency)
和关联关系不同的是,依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
- A 类是 B 类中的(某中方法的)局部变量;
- A 类是 B 类方法当中的一个参数;
- A 类向 B 类发送消息,从而影响 B 类发生变化;
数据类型
八个基本类型对应的包装类
int(4字节) | Integer |
byte(1字节) | Byte |
short(2字节) | Short |
long(8字节) | Long |
float(4字节) | Float |
double(8字节) | Double |
char(2字节) | Character |
boolean(未定) | Boolean |
自动装箱拆箱
在前面的文章中提到,Java为每种基本数据类型都提供了对应的包装器类型,至于为什么会为每种基本数据类型提供包装器类型在此不进行阐述,有兴趣的朋友可以查阅相关资料。在Java SE5之前,如果要生成一个数值为10的Integer对象,必须这样进行:
Integer i =
new
Integer(
10
);
而在从Java SE5开始就提供了自动装箱的特性,如果要生成一个数值为10的Integer对象,只需要这样就可以了:
Integer i =
10
;
这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。
那什么是拆箱呢?顾名思义,跟装箱对应,就是自动将包装器类型转换为基本数据类型:
Integer i =
10
;
//装箱
int
n = i;
//拆箱
简单一点说,装箱就是 自动将基本数据类型转换为包装器类型;拆箱就是 自动将包装器类型转换为基本数据类型。
Java缓存池
Integer int_01 = 123;
Integer int_02 = 123;
System.out.println(int_01 == int_02); //true
Integer int_03 = new Integer(123);
Integer int_04 = new Integer(123);
System.out.println(int_03 == int_04); //false
对于 “ == ”号咱么都知道,当 “== ”比较的是引用对象的时候,其实比较的是两个对象的内存地址,如果是比较基本类型数据的时候比较的是具体值。
Integer a = 123;
Integer b = new Integer(123);
Integer c = Integer.valueOf(123);
System.out.println(a == b); //false
System.out.println(a == c); //true
System.out.println(b == c); //false
当我们传入一个在-128到127之间的整数时,编译器会在缓冲池中取出整数对应的Integer对象,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象,否则会创建一个新的对象。比如下面这样:
编译器会在缓冲池范围内的基本类型自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
基本类型对应的缓冲池如下:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
超出缓存池外
Integer m = 323;
Integer n = 323;
System.out.println(m == n); // false
String
String 被声明为 final,因此它不可被继承。
内部使用 char 数组存储数据,该数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
String 不可变好处
什么是不可变
String s1 = "Hello";
s1 = s1 + " World"; // 实际上创建了一个新的字符串对象
System.out.println(s1); // 输出: "Hello World"
这里的输出确实是 "Hello World"
,但这并不意味着原字符串被修改了。实际发生的情况是:
String s1 = "Hello";
创建了一个字符串对象"Hello"
,并将它的引用赋给变量s1
。s1 = s1 + " World";
-
- 运算符
+
在字符串中会触发字符串连接操作。 - 实际上,
s1 + " World"
会创建一个新的字符串对象"Hello World"
。 - 变量
s1
被重新赋值,指向了新创建的字符串对象。 - 原来的字符串
"Hello"
没有被修改,仍然保留在内存中。
- 运算符
System.out.println(s1);
打印的是变量s1
当前指向的字符串,即"Hello World"
。
常量字符串(比如直接写 String s = "Hello";
)存储在字符串常量池中,常量池是一个优化内存的机制,避免了重复创建相同字符串的问题。但字符串池中的字符串是全局可用的,直到 JVM 关闭时才释放。
不可变性不会引起内存泄漏:
尽管 String
是不可变的,但 Java 的垃圾回收机制会回收那些没有引用指向的字符串对象。例如原来的 "Hello"
对象如果没有其他引用指向它,会被垃圾回收(GC)回收,不会造成内存泄漏。
1. 可以缓存 hash 值
因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
2. String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
Java 在堆内存中维护了一个字符串常量池。如果一个字符串已经存在,那么 JVM 会直接返回池中现有的引用,而不是创建新的对象。
节省内存:字符串不可变性允许复用同一个对象。例如,String s1 = "hello"; String s2 = "hello";
中,s1
和 s2
指向同一个字符串对象。
- 避免副作用:如果字符串是可变的,修改其中一个实例可能会影响池中的其他引用。
3. 安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
4. 线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
String 的 final
String StringBuffer StringBuilder
1. 可变性
- String 不可变
- StringBuffer 和 StringBuilder 可变
2. 线程安全
- String 不可变,因此是线程安全的
- StringBuilder 不是线程安全的
- StringBuffer 是线程安全的,内部使用 synchronized 进行同步
String.intern()
使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象。
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用。intern() 首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
System.out.println(s1.intern() == s3); // true
如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。
String s4 = "bbb";
String s5 = "bbb";
System.out.println(s4 == s5); // true