Java - 面向对象
转载声明
# 一、面向对象和面向过程的区别
面向过程:
面向过程的思想会将一件事情,拆分成一个个更具体的小任务,由一个个方法去完成他们。只要按顺序执行这些方法,就能完成这件事情。
面向对象:
面向对象的思想会把事物抽象成对象的概念,把一件事情拆分成一个个更具体的小问题,交由不同的对象分别完成以解决问题。
更详细生动的说明详见 2分钟让你明白什么是面向对象编程 (opens new window)
| 项目名称 | 面向对象程序设计 | 面向过程程序设计(也叫结构化编程) |
|---|---|---|
| 定义 | 面向对象顾名思义就是把现实中的事务都抽象成为程序设计中的“对象”,其基本思想是一切皆对象,是一种“自下而上”的设计语言,先设计组件,再完成拼装。 | 面向过程是“自上而下”的设计语言,先定好框架,再增砖添瓦。通俗点,就是先定好main()函数,然后再逐步实现main()函数中所要用到的其他方法。 |
| 特点 | 封装、继承、多态 | 算法+数据结构 |
| 优势 | 适用于大型复杂系统,方便复用、 | 适用于简单系统,容易理解 |
| 劣势 | 比较抽象、性能比面向过程低 | 难以应对复杂系统,难以复用,不易维护、不易扩展 |
| 对比 | 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 | 性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素 |
| 设计语言 | Java、Smalltalk、EIFFEL、C++、Objective-、C#、Python等 | C、Fortran |
因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。
因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
面向对象易维护、易复用、易扩展。面向过程性能相对较高。
# 二、面向对象三大特征
# 2.1 封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
封装的好处
- 隐藏实现细节,提供公共的访问方式
- 提高代码的复用性
- 提高安全性(别人不能通过 变量名.属性名 的方式来修改私有的成员属性)
public class Student {
private int id;//id属性私有化
private String name;//name属性私有化
//获取id的方法
public int getId() { return id; }
//设置id的方法
public void setId(int id) { this.id = id; }
//获取name的方法
public String getName() { return name; }
//设置name的方法
public void setName(String name) { this.name = name; }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.2 继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
继承的优点
- 提高代码的复用性
- 提高代码的易维护性
- 提高代码的可扩展性
- 使代码结构清晰
继承的缺点
- 高耦合性:父子类联系紧密,修改父类的同时,需要兼顾子类
- 破坏封装性、降低灵活性
# 2.3 多态
表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
多态存在的必要条件
- 要有继承
- 要有重写
- 父类引用指向子类对象
多态的好处
- 提高了代码的可扩展性
- 可替换性
- 接口性:向子类提供共同接口,由子类来完善或覆盖
- 灵活性
- 简化性
# 三、对象和引用
# 3.1 对象实体和对象引用
一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
# 3.2 对象相等和引用相等
对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。
# 3.3 四种引用,强弱软虚
强引用
强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用 方式:
String str = new String("str"); System.out.println(str);1
2软引用
软引用在程序内存不足时会被回收,使用方式:
// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的, // 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T SoftReference wrf = new SoftReference(new String("str"));1
2
3可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建 的对象。
弱引用
弱引用就是只要JVM垃圾回收器发现了它就会将之回收,使用方式:
WeakReference wrf = new WeakReference(str);1可用场景: Java源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用,我的理解就是, 一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
虚引用
虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。注意 哦,其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多 被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue , 使用例子:
PhantomReference prf = new PhantomReference(new String("str"), new ReferenceQueue<>());1可用场景: 对象销毁前的一些操作,比如说资源释放等。 Object.finalize() 虽然也可以做这 类动作,但是这个方式即不安全又低效
# 3.4 创建对象的四种方式
- new创建新对象
- 通过反射机制
- 采用clone机制
- 通过序列化机制
# 四、抽象类和接口的区别
- 抽象类中可以定义构造函数,接口不能定义构造函数
- 抽象类中可以有抽象方法和具体方法,接口中只能有抽象方法
- 抽象类中的成员权限可以是
public、默认、protected,抽象方法不能被private修饰,具体方法可以;接口中的成员只能是public - 抽象类中可以包含静态方法(具体方法可以是静态的,抽象方法不行),接口中不能包含静态方法(因为接口中定义的方法就是为了让实现类去实现,但是静态方法必须在编译时实现)
JDK8 的改变
- 允许在接口中包含具体实现的方法,使用
default修饰 - 因为允许具体实现的方法,接口中也就允许有静态方法
# 五、比较方法
# 5.1 == 和 equals() 的区别
== 对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==比较的是值。 - 对于引用数据类型来说,
==比较的是对象的内存地址。
Java 只有值传递
所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals() 作用不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。
Object.equals()
如果没有对该方法进行重写,Object 中的 equals() 方法仍然是 == 比较。
# 5.2 hashCode() 与 equals()
因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
更多解释见 Java基础常见知识&面试题总结(上) | JavaGuide (opens new window)
hashCode的作用
hashCode和equals有一个相同的作用,就是用作比较:
- equals 为true的两个对象相等,false则两个对象不相等
- hashCode 相同的两个对象不一定相等,但不同的两个对象一定不相等
为什么要有 hashCode?
效率 当一个对象逐渐复杂时,equals方法中的判定条件也会逐渐复杂,这个时候比较两个对象是否相等依旧使用equals的话,就会变得低效。
因此引出hashCode,通过一定的计算方式,为每个对象分配一个“哈希码”,让相等的对象都拥有相同的哈希码。之后需要比较两个对象的时候,先比较哈希码,如果哈希码都不相同,两个对象一定不相等。如果哈希码相同,再继续使用equals确定对象是否相等。
hashCode 方法相对与 equals 会更加高效,通过这两步判断,来确定两个对象是否相等
# 六、构造方法
# 6.1 构造方法的特点
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
Tips
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
# 6.2 无参构造函数的作用
- 不写任何构造器,也会有一个默认的无参构造。但如果写了有参构造,就必须另写一个无参构造,否则就无法执行无参构造。
- 子类的构造当中,如果没有指定 super() 来调用父类中某个构造方法,则会调用父类中的无参构。如果这个时候父类中存在有参构造而没有无参构造,就会出错。
# 6.3 构造器是否可以被重写?
父类的私有属性、方法以及构造器都是不能被重写的。
# 七、 String、StringBuffer、StringBuilder
# 7.1 String 为什么是不可变的
详细参考 String为什么不可变 - 梦醒点灯 (opens new window)
在Java中String类其实就是对字符数组的封装。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}
2
3
4
- 保存字符串的数组被
final修饰且为私有的,并且String类没有提供/暴露修改这个字符串的方法。 String类被final修饰导致其不能被继承,进而避免了子类破坏String不可变。
# 7.2 String 真的不可变吗?
以下内容转载自 String为什么不可变 - 梦醒点灯 (opens new window)
String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗?
反射
public static void testReflection() throws Exception {
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
打印结果为:
s = Hello World
s = Hello_World
2
在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的【不可变】对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。
# 7.3 StringBuffer、StringBuilder
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
# 7.4 线程安全性
Tips
String、StringBuffer 是线程安全的,StringBuilder 非线程安全。
String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
# 7.5 性能
StringBuilder > StringBuffer > String
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
- 操作少量的数据: 适用
String - 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder - 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
# 八、 Object 类的常见方法
//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public final native Class<?> getClass()
//native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public native int hashCode()
//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
public boolean equals(Object obj)
//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
protected native Object clone() throws CloneNotSupportedException
//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public String toString()
//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notify()
//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void notifyAll()
//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final native void wait(long timeout) throws InterruptedException
//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
public final void wait(long timeout, int nanos) throws InterruptedException
//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
public final void wait() throws InterruptedException
//实例被垃圾回收器回收的时候触发的操作
protected void finalize() throws Throwable { }
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
# 九、深拷贝和浅拷贝、引用拷贝
Tips
浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指 向原来的对象。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象
深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
引用拷贝就是两个不同的引用指向同一个对象。
