面向对象基础
对象实体与对象引用有什么不同?
new创建的对象实例会保存在堆内存中,对象引用指向对象实例(对象引用存放在栈内存中)
- 一个对象引用可以指向0个或1个对象
- 一个对象可以有n个引用指向它
对象的相等和引用相等的区别
- 对象的相等一般比较的是
内存中存放的内容是否相等 - 引用相等一般比较的是他们指向的
内存地址是否相等
String str1 = "hello";
String str2 = new String("hello");
String str3 = "hello";
// 使用==比较字符串的引用相等(字符串使用==比较的是地址,要比较值得用equals方法)
System.out.println(str1 == str2); // false
System.out.println(str1 == str3); // true
// 使用equals比较字符串内容的相等
System.out.println(str1.equals(str2)); // true
System.out.println(str1.equals(str3)); // true
一个类没有声明构造器,该程序能正常执行吗?
可以执行,因为即使不声明也会默认带有无参构造器,但是!如果我们重载了有参构造器,那么无论我们是否用到无参构造器都必须将其在类里显式的写出来。
构造器的特点?是否能被重写(override)?
- 名字与类名相同
- 无返回值,但是不能使用void来声明构造器
- 生成类的对象时会自动执行,不需要调用
构造器不能被override,但是能被overload(重载),类中可以有多个构造器。
面向对象三大特征?
封装
把对象的属性隐藏在对象内部(private),不允许外部对象直接访问对象的内部信息,但是可以通过提供一些可被外界调用的方法来操作属性。
继承
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类无法访问,只是拥有
- 子类可以拥有自己的属性和方法,即:子类可以对父类进行扩展
- 子类可以自己的方式实现父类的方法
多态
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系
- 引用类型变量发出的方法调用到底是哪个类中的方法,必须在程序运行期间才能确定
- 多态不能调用只在子类存在但在父类不存在的方法
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法
接口和抽象类的共同点?区别?
共同点
- 都不能被实例化
- 都可以包含抽象方法
- 都可以有默认实现的方法(在
Java8可以使用default关键字在接口中定义默认方法)
区别
- 接口主要用于对类的行为进行约束,当我们实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系
- 一个类只能继承一个类,但是一个类可以实现多个接口
- 接口中的成员变量只能是
public static final类型的,不能被修改且必须有初始值。抽象类的成员变量默认是default,可在子类中被重新定义,也可被重新赋值
深拷贝和浅拷贝的区别?什么是引用拷贝?
- 浅拷贝:浅拷贝会在堆上创建一个新对象(区别于引用拷贝的一点),但是如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,即拷贝对象和原对象共用同一个内部对象
- 深拷贝:深拷贝会完全复制整个对象,包括该对象所包含的内部对象
浅拷贝
Address
public class Address implements Cloneable {
private String name;
public Address() {
}
public Address(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
Person
public class Person implements Cloneable {
private Address address;
public Person(Address address) {
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public Address getAddress() {
return address;
}
}
测试
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person(new Address("广州"));
Person person1Clone = (Person) person1.clone();
System.out.println(person1.getAddress() == person1Clone.getAddress()); // true
}
}
person1Clone和person1使用的是同一个Address对象
深拷贝
Address
public class Address implements Cloneable {
private String name;
public Address() {
}
public Address(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
Person
修改了Person类的clone方法,在复制的时候连带着要把Person对象内部的Address对象一起复制
public class Person implements Cloneable {
private Address address;
public Person(Address address) {
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
try {
Person person = (Person) super.clone();
person.setAddress((Address) person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
private void setAddress(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
}
测试
因为我们修改了Person的clone方法,使得其内部的Address对象在复制的时候不再是复制Address对象的引用地址,而是复制整个对象,所以person1的Address和person1Clone的Address地址不一致
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person(new Address("广州"));
Person person1Clone = (Person) person1.clone();
System.out.println(person1.getAddress() == person1Clone.getAddress()); // false
}
}
引用拷贝
两个不同的引用指向同一个对象,浅拷贝会在堆上创建一个对象,引用拷贝则不会,引用拷贝的拷贝对象和原对象地址一致,共用同一个对象
Object
Object常见方法
11种
/**
* native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,所以不允许子类重写
*
* @return
*/
public final native Class<?> getClass();
/**
* native方法,用于返回对象的哈希码,主要是用在哈希表中,比如JDK中的HashMap
*
* @return
*/
public native int hashCode();
/**
* 用于比较两个对象的内存地址是否相等,String类对该方法进行了重写,以用于比较字符串的值是否相等
*
* @param obj
* @return
*/
public boolean equals(Object obj) {
return (this == obj);
}
/**
* native方法,用于创建并返回当前对象的一份拷贝
*
* @return
* @throws CloneNotSupportedException
*/
protected native Object clone() throws CloneNotSupportedException;
/**
* 返回类的名字实例的哈希码的16进制的字符串,建议Object所有的子类都重写这个方法
*
* @return
*/
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
/**
* native方法,并且不能重写(被final修饰),唤醒一个在此对象监视器上等待的线程(监视器相当于锁的概念)
* 如果有多个线程在等待只会任意唤醒一个
*/
public final native void notify();
/**
* native方法,并且不能重写,跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,
* 而不是一个线程
*/
public final native void notifyAll();
/**
* native方法,并且不能重写,暂停线程的执行,注:sleep方法没有释放锁,而wait方法释放了锁,
* timeout是等待时间
*
* @param timeout
* @throws InterruptedException
*/
public final native void wait(long timeout) throws InterruptedException;
/**
* 多了nanos参数,这个参数表示额外时间(以纳秒为单位,范围是0-999999),所以超时的时间还需要加上nanos纳秒
*
* @param timeout
* @param nanos
* @throws InterruptedException
*/
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
/**
* 和上面的两个wait方法一样,只不过该方法一直等待,没有超时概念
*
* @throws InterruptedException
*/
public final void wait() throws InterruptedException {
wait(0);
}
/**
* 实例被垃圾回收器(gc)回收的时候触发的操作
*
* @throws Throwable
*/
protected void finalize() throws Throwable {
}
equals()
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。
Object 类 equals() 方法:
public boolean equals(Object obj) {
return (this == obj);
}
equals() 方法存在两种使用情况:
- 类没有重写
equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object类equals()方法。 - 类重写了
equals()方法:一般我们都重写equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
String类的equals()是被重写过的
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
hashCode()有什么用?
作用:获取哈希码(int整数),也称为散列码,确定该对象在哈希表中的索引位置
hashCode()定义在Object类中,即Java中所有的类都包含这个函数
hashCode()这个方法在Object类中被native关键字修饰,说明他是一个本地方法,也就是用C语言或C++实现的
/**
* native方法,用于返回对象的哈希码,主要是用在哈希表中,比如JDK中的HashMap
*
* @return
*/
public native int hashCode();
散列表存储的是键值对(key-value),特点是能根据key快速找到对应的value,而这其中就用到了散列码
为什么要有hashCode()?
摘自《Head First Java》
当你把对象加入
HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode值作比较,如果没有相符的hashCode,HashSet会假设对象没有重复出现。但是如果发现有相同
hashCode值的对象,这时会调用equals()方法来检查hashCode相等的对象是否真的相同。如果两者相同,
HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了
equals的次数,相应就大大提高了执行速度。
其实hashCode()和equals()都是用于比较两个对象是否相等的
为什么JDK要同时提供他们两个?
效率问题。
在一些容器比如HashMap、HashSet中,有了hashCode()之后,判断元素是否在对应容器中效率会更高
如果HashSet在对比的时候,同样的hashCode有多个对象,他才会继续使用equals()来判断内容是否真的相同,即hashCode帮助我们大大缩小了查找重复对象的成本
为什么不只提供hashCode()?
因为hashCode值相等并不代表两个对象就相等,hashCode()所使用的哈希算法也许刚好会让多个对象传回相同的哈希值,越糟糕的哈希算法越容易发生这种碰撞,但这也与数据值域分布的特性有关(所谓的哈希碰撞也就是指的不同的对象得到相同的hashCode)
总结
- 若两个对象的
hashCode值相等,那这两个对象不一定相等(哈希碰撞) - 若两个对象的
hashCode值相等并且equals()判断也为true,那才认为两个对象相等 - 若两个对象的
hashCode值不相等,则可以直接认为两个对象不相等
为什么重写equals()时必须重写hashCode()方法
因为在使用equals()去判断两个对象相等时,他们的hashCode也必须要相等,如果不重写equals()就会导致其判断是两个相等的对象但是hashCode值却不相等
总结
equals方法判断两个对象是相等的,那这两个对象的hashCode值也要相等- 两个对象有着相同的
hashCode值,他们也不一定是相等的(哈希碰撞)
String
StringBuffer和StringBuilder的区别?
可变性
String是不可变的StringBuffer与StringBuilder都继承自AbstractStringBuilder类,在AbstractStringBuilder类中也是使用字符数组保存字符串,不过没有使用final和private关键字修饰,最关键的是这个类还提供了很多修改字符串的方法比如append// StringBuilder类 public final class StringBuilder extends AbstractStringBuilder implements Serializable, CharSequence // StringBuffer类 public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequenceAbstractStringBuilder
abstract class AbstractStringBuilder implements Appendable, CharSequence { /** * The value is used for character storage. */ char[] value; /** * The count is the number of characters used. */ int count; 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; } // ...... }
线程安全性
String中的对象是不可变的,即可理解为常量,线程安全,AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义类一些字符串的基本操作,例如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的@Override public synchronized StringBuffer append(Object obj) { toStringCache = null; super.append(String.valueOf(obj)); return this; }StringBuilder并没有对方法进行加同步锁,所以是非线程安全的@Override public StringBuilder append(Object obj) { return append(String.valueOf(obj)); }
性能
- 每次对
String类型进行改变的时候,都会生成一个新的String对象,然后指针指向新的String对象。 StringBuffer每次都会对StringBuffer本身进行操作,而不是生成新的对象并改变对象引用- 相同的情况下使用
StringBuilder相比使用StringBuffer仅能获得10% ~ 15%左右的性能提升,但是却要冒线程不安全的风险
- 每次对
总结
- 操作少量数据:
String类 - 单线程操作字符串缓冲区下大量数据操作:
StringBuilder类 - 多线程操作字符串缓冲区下大量数据操作:
StringBuffer类
String为什么是不可变的?
我们来看看String类
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
// ......
}
看到了吗,
String类被final关键字修饰,而被final关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final关键字修饰的数组保存字符串并不是String不可变的根本原因,因为这个数组保存的字符串是可变的(引用类型变量的情况)根本原因:
保存字符串的数组被
final修饰且为private,并且String类并没有提供/暴露修改这个字符串的方法private final char value[];
String类被final修饰导致其不能被继承,进而避免了子类破坏String不可变public final class String implements java.io.Serializable, Comparable<String>, CharSequence {扩展:如果
String可变会导致什么样的结果?用
HashSet做示例,其内的元素是StringBuilderpublic static void main(String[] args) { HashSet<StringBuilder> set = new HashSet<>(); StringBuilder stringBuilder1 = new StringBuilder("aaa"); StringBuilder stringBuilder2 = new StringBuilder("aaabbb"); set.add(stringBuilder1); set.add(stringBuilder2); System.out.println(set); // [aaabbb, aaa] StringBuilder stringBuilder3 = stringBuilder1; stringBuilder3.append("bbb"); System.out.println(set); // [aaabbb, aaabbb] }
StringBuilder型变量stringBuilder1和stringBuilder2分别指向了堆内的字面量”aaa”和”aaabbb”,把他们都插入到HashSet中,但是我们后面定义了一个stringBuilder3指向stringBuilder1的地址,再改变stringBuilder3的值,因为StringBuilder并不具备不可变性的保护,这就导致了stringBuilder3直接在原来的stringBuilder1地址上修改导致stringBuilder1的值也变了,此时HashSet里面竟然会有两个一样的键值,破坏了HashSet键值的唯一性,所以千万千万不要用可变类型做HashMap和HashSet键值补充:在Java9之后,
String、StringBuilder、StringBuffer的实现改用为byte数组public final class String implements java.io.Serializable,Comparable<String>, CharSequence { // @Stable 注解表示变量最多被修改一次,称为“稳定的”。 @Stable private final byte[] value; } abstract class AbstractStringBuilder implements Appendable, CharSequence { byte[] value; }提问:为什么Java9的
String底层要把char[]改为byte[]?新版的
String其实支持两个编码方案:Latin-1和UTF-16,如果字符串中包含的汉字没有超过Latin-1可表示范围内的字符,那就会使用Latin-1作为编码方案,在该编码方案下,byte占一个字节(8位)char占两个字节(16位),byte相比char节省一般的内存空间如果字符串中包含的汉字超过
Latin-1可表示范围内的字符,那么byte和char占用的内存空间是一致的
字符串拼接用“+”还是用StringBuilder?
Java语言本身不支持运算符重载,“+”与“+=”是专门为String类重载过的运算符,也是Java中仅有的两个重载过的运算符
public static void main(String[] args) {
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;
}
来看看它的字节码文件
可以看出字符串对象通过“+”的字符串拼接方式,实际上是通过StringBuilder``调用append()方法实现的,拼接完成后调用StringBuilder里的toString()得到一个String对象
在循环里使用“+”进行拼接会发生什么?
public static void main(String[] args) {
String[] arr = new String[]{"he", "llo", "world"};
String str = "";
for (String s : arr) {
str += s;
}
}
查看字节码我们可以得知,使用“+”的方式拼接字符串,他会在循环内部不断创建StringBuilder对象
直接使用StringBuilder对象去拼接字符串就不会存在这个问题了
public static void main(String[] args) {
String[] arr = new String[]{"he", "llo", "world"};
StringBuilder stringBuilder = new StringBuilder();
for (String s : arr) {
stringBuilder.append(s);
}
}
String.equals() 和 Object.equals()的区别?
String.equals()
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
Object.equals()
public boolean equals(Object obj) {
return (this == obj);
}
String中的equals方法是被重写过的,用来比较字符串的值是否相等,而Object中的equals方法比较的是对象的内存地址。
字符串常量池的作用?
为了避免字符串的重复创建
字符串常量池是JVM为了提升性能和减少内存针对字符串(String类)专门开辟的一块区域
我们来看看以下两个字符串的地址
public static void main(String[] args) {
// 在堆中创建字符串对象"ab"
// 将字符产对象"ab"的引用保存在字符串常量池中
String str1 = "ab";
// 直接返回字符串常量池中字符串对象"ab"的引用
String str2 = "ab";
System.out.println("str1: " + String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(str1)));
System.out.println("str2: " + String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(str2)));
System.out.println(str1 == str2);
}
可以看到str1和str2的地址是完全一致的
String str1 = new String(“abc”);这段代码创建了几个字符串对象?
会创建 1 或 2 个字符串对象
如果字符串常量池中不存在字符串对象”abc”的引用,那么他会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中
String str1 = new String("abc");
LDC:用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话就直接返回,如果没有就会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量中如果字符串常量池中有”abc”的引用,则只会在堆中创建一个字符串对象”abc”
String string = "abc"; String str1 = new String("abc");
下面的LDC命令不会在堆中创建新的字符串对象”abc”,这是因为在上面已经执行了一次LDC命令,已经在堆中创建过一次字符串对象”abc”了,所以会直接返回字符串常量池中字符串对象”abc”的引用
String.intern()的作用?
public native String intern();
String.intern()是一个被native修饰的方法,作用是将指定的字符串对象的引用保存在字符串常量池中
如果字符串常量池中保存了对应的字符串对象的引用,则直接返回该引用
String s1 = "Java"; String s2 = s1.intern(); System.out.println("s1: " + String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(s1))); System.out.println("s2: " + String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(s2))); System.out.println(s1 == s2);
s2返回了s1在字符串常量池保存的对应的字符串对象的引用,所以二者地址一致
如果字符串常量池中没有保存对应的字符串对象的引用,则会在常量池中创建一个指向该字符串对象的引用并返回
// 会在堆中在单独创建一个字符串对象 String s3 = new String("Java"); // 直接返回字符串常量池中字符串对象"Java"对应的引用 String s4 = s3.intern(); System.out.println("s3: " + String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(s3))); System.out.println("s4: " + String.class.getName() + "@" + Integer.toHexString(System.identityHashCode(s4))); System.out.println(s3 == s4);
因为s3使用了
new关键字创建了新的String对象在对其赋值”abc”字符串
String类型的变量和常量做“+”运算的时候发生了什么?
不加final关键字时
String str1 = "str";
String str2 = "int";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4); // false
System.out.println(str3 == str5); // true
System.out.println(str4 == str5); // false
Considerations
这里比较的是对象的内存地址,而不是字符串的值
得益于编译器的优化,对于编译期可以确定值得字符串,就是常量字符串,JVM会将其存入字符串常量池中,并且字符串常量拼接得到的字符串常量在编译阶段就已经被存放在字符串常量池了
常量折叠:把常量表达式得值求出来作为常量嵌在最终生成的代码中,这是Javac编译器哦会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)
对于上面的String str3 = "str" + "ing",编译器会帮我们优化成:String str3 = "string"
并非所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值得常量才可以,例如:
- 基本数据类型(
byte、boolean、short、char、int、float、long、double)以及字符串常量 final修饰的基本数据类型和字符串变量- 字符串通过“+”拼接得到的字符串、基本数据类型之间的算术运算(加减乘除)、基本数据类型的位运算(
<<、>>、>>>)
对于在程序编译期间无法确定的值,编译器是无法对其进行优化的
例如上面的变量str4,对象引用和“+”的字符串拼接方式,实际上是通过StringBuilder调用append()方法实现的,拼接完成之后调用toString()得到一个String对象
String str4 = str1 + str2;
// 等价于
String str4 = new StringBuilder().append(str1).append(str2).toString();
我们在日常写代码的时候,应该尽量避免多个字符串对象拼接,因为这样会重新创建对象,如果需要改变字符串的时候,可以使用
StringBuilder或者StringBuffer
加入final关键字时
对字符串使用final关键字修饰后,可以让编译器当作常量来访问
final String str1 = "str";
final String str2 = "ing";
String stringA = "str" + "ing";
String stringB = str1 + str2;
System.out.println(stringA == stringB);
运行结果:
这是因为被final关键字修饰后的String字符串会被编译器当作常量来处理,编译器在程序编译期就可以确定他的值,其效果就相当于访问常量
如果我们把常量str2改成运行时才能知道它的值,就没有办法使用常量折叠来进行优化了(和方法加不加final来修饰无关,就算加了也不可以优化)
public static void main(String[] args) {
final String str1 = "str";
final String str2 = getStr();
String stringA = "str" + "ing";
String stringB = str1 + str2;
System.out.println(stringA == stringB);
}
public static String getStr() {
return "ing";
}
运行结果:false