面向对象
面向对象和面向过程
区别,解决问题的方式不同:
- 面向过程把解决问题的过程拆成一个个方法,通过方法的执行调用来解决问题
- 面向对象会先抽象出对象,再用对象执行方法的方式解决问题
对象实体和对象引用
new 对象实例(在堆内存中)
对象引用指向对象实例(对象引用存放在栈内存)
对象相等和引用相等
对象相等:比较的是内存中存放的内容是否相等
引用相等:比较的是指向的内存地址是否相等
类的构造方法作用
构造方法目的是完成对象的初始化工作
如果一个类没有声明构造方法,系统会有默认的不带参数的构造方法。如果自己添加了类的构造方法,无论是否有参,JAVA就不会再添加默认的无参数的构造方法。
构造方法的特点
- 名字和类名相同
- 没有返回值,不能用void声明构造函数
- 生成类的对象时自动执行,不需要调用
- 不能被重写(override),可以被重载(overload)
多态的特点
- 对象类型和引用类型之间具有继承类、实现接口的关系
- 引用类型变量发出的方法调用的是哪个类中的方法,需要在程序运行期间才能确定
- 多态不能调用“只在子类存在但父类不存在的方法”
- 子类重写了父类方法,则执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的就是父类的方法
多态:具体表现是父类的引用指向子类的实例
接口和抽象类
共同点:
- 都不能被实例化
- 都可以包含抽象方法
- 都有默认实现的方法
区别:
接口主要对类的行为进行约束,实现接口就具有了对应的行为;
抽象类主要是代码复用,强调所属关系。
一个类只能继承一个类;一个类可以实现多个接口。
接口的成员变量只能被:public static final 修饰,不能被修改且必须有初值;
抽象类的成员默认是default,可以在子类中被重新定义,也可以被重新赋值。
深拷贝和浅拷贝
深拷贝
完全复制整个对象,包括这个对象所包含的内部对象
浅拷贝
会在堆上创建一个新对象,这点跟引用拷贝不一样。如果原对象内部的属性是引用类型,则浅拷贝会直接复制内部对象的引用地址,则表示:拷贝对象和原对象共用一个内部对象。
public class Address implements Cloneable{
private String name;
// 省略构造函数、Getter&Setter方法
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Person implements Cloneable {
private Address address;
// 省略构造函数、Getter&Setter方法
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
------
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());
----------------------------------------------------------------------
更改clone方法:
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
----
Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());
这个深拷贝
引用拷贝
引用拷贝是两个不同的引用指向同一个对象
Java常见类
Object常见方法
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* naitive 方法,用于创建并返回当前对象的一份拷贝。
*/
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 { }
==和equals()
==
对基本类型和引用类型作用不同:
基本数据类型:== 比较值的大小
引用数据类型:== 比较的是对象的内存地址
注:
JAVA只有值传递,没有引用传递。对于==,不管是哪种数据类型,本质比较的是值。
equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。存在于Object类中,而Object类是所有类的直接或间接父类,因此所有类都有equals()方法。
两种使用情况:
类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
public boolean equals(Object obj) {
return (this == obj);
}
类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
hashCode() 的作用
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。
注意: Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
为什么 JDK 还要同时提供hashCode() 和 equals()
因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。那为什么不只提供 hashCode() 方法呢?这是因为两个对象的hashCode 值相等并不代表两个对象就相等。那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。总结下来就是 :如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
重写 equals() 时必须重写 hashCode() 方法
因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
String、StringBuffer、StringBuilder
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
三者使用的情况
操作少量的数据: 适用 String
单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
String是不可变的
public final class String implements java.io.Serializable,
Comparable<String>, CharSequence {
private final char value[];
//...
}
被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。
String 真正不可变有下面2点原因:
保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
Java 9 将 String 的底层实现由 char[] 改成了 byte[]
新版的 String 其实支持两个编码方案: Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,byte 和 char 所占用的空间是一样的。
+和StringBuilder
字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。
注:
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
String#equals() 和 Object#equals() 区别
String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Object 的 equals 方法是比较的对象的内存地址。
String s1 = new String(“abc”);这句话创建了1/2个字符串对象
会创建 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。
2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
intern 方法的作用
tring.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以分为两种情况:
如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
String 类型的变量和常量做“+”运算
比较 String 字符串的值是否相等,可以使用 equals() 方法。 String 中的 equals 方法是被重写过的。 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是字符串的值是否相等。如果你使用 == 比较两个字符串是否相等的话,IDEA 还是提示你使用 equals() 方法替换。
常量折叠:
常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
eg:
对于 String str3 = “str” + “ing”; 编译器会给你优化成 String str3 = “string”;
只有编译器在程序编译期就可以确定值的常量才可以:基本数据类型( byte、boolean、short、char、int、float、long、double)以及字符串常量。final 修饰的基本数据类型和字符串变量字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。