Java常见类详解
# Java 常见类详解
# Object
# Object 类的常见方法有哪些?
Object
类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
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
33
34
35
36
37
38
39
40
41
42
43
44
# == 和 equals() 的区别
1、==
对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==
比较的是值。 - 对于引用数据类型来说,
==
比较的是对象的内存地址。
因为 Java 只有值传递,所以,对于
==
来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
2、equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
equals()
方法存在两种使用情况:
类没有重写
equals()
方法时:- 等价于通过“==”比较这两个对象,使用的默认是
Object
类equals()
方法。 - 即比较的是内存地址。
- 等价于通过“==”比较这两个对象,使用的默认是
类重写了
equals()
方法:一般我们都重写
equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即认为这两个对象相等)。即比较的是内容,比如
String
中的equals
方法就是被重写过的。
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb); // true
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
System.out.println(42 == 42.0); // true
2
3
4
5
6
7
8
# 为什么 Java 中只有值传递?
Java 中只有值传递,是因为 Java 中的变量(包括基本类型和引用类型)实际上只是存储在内存中的值,并没有直接指向对象的内存地址。
# hashCode() 有什么用?
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode()
方法的主要作用是为了提高哈希表的性能,同时也经常用于对象的比较和判等。
# 为什么要有 hashCode?
**因为对于一个类的实例,如果要将其存储到哈希表中,就需要实现 hashCode()
方法,以便在计算哈希值时能够正确地反映对象的属性。**同时,由于哈希表中可能会有哈希冲突(即不同对象的哈希码相同),因此还需要实现 equals()
方法来判断两个对象是否相等。
hashCode()
和equals()
都是用于比较两个对象是否相等。
1、那为什么 JDK 还要同时提供这两个方法呢?
这是因为在一些容器(比如 HashMap
、HashSet
)中,有了 hashCode()
之后,判断元素是否在对应容器中的效率会更高。
也就是说 hashCode
帮助我们大大缩小了查找成本。
2、那为什么不只提供 hashCode()
方法呢?
这是因为两个对象的 hashCode
值相等并不代表两个对象就相等。
3、那为什么两个对象有相同的 hashCode
值,它们也不一定是相等的?
因为 hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。
越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode
)。
总结下来就是:
- 如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。 - 如果两个对象的
hashCode
值相等并且equals()
方法也返回true
,我们才认为这两个对象相等。 - 如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。
# 为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
# 重写 equals()
时没有重写 hashCode()
方法的话,使用 HashMap
可能会出现什么问题?
如果在重写 equals()
方法的同时没有重写 hashCode()
方法,那么在将对象存储到 HashMap 中时,可能会出现以下问题:
- 相等的对象返回不同的哈希码:如果两个对象在 equals() 方法中被认为是相等的,但是它们的 hashCode() 方法返回的哈希码不同,那么它们将会被存储在 HashMap 中的不同位置,这样可能会导致在查找或删除元素时出现问题。
- 不同的对象返回相同的哈希码:如果两个对象在 equals() 方法中被认为不相等,但是它们的 hashCode() 方法返回的哈希码相同,那么它们将会被存储在 HashMap 中的同一个位置,这样可能会导致在查找或删除元素时出现问题。
简单来说就是会出现两个问题:无法正常存储对象和无法正常查找对象。
因此,在重写 equals() 方法的同时,也应该重写 hashCode() 方法,以确保相等的对象具有相同的哈希码,不相等的对象具有不同的哈希码。这样可以保证 HashMap 的正确性和性能。
# String
# String、StringBuffer 和 StringBuilder 的区别是什么?
String
是不可变的,每次对字符串操作都会生成一个新的字符串对象;- 而
StringBuffer
和StringBuilder
是可变,可以在原有对象的基础上进行修改,因此能够提高程序的执行效率。
String
中的对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer
是线程安全的,它的方法都是同步的,依次可以在多线程环境下安全使用但执行速度较慢;
而 StringBuilder
是线程不安全的,它的方法不是同步的,因此在单线程环境下的执行速度比 StringBuffer
快。
# String 为什么是不可变的?
- 保存字符串的数组被
final
修饰且为私有,并且String
类没有提供修改这个字符串的方法。 String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
的可能性。
除此之外,String 对象的不可变是由于对 String 类型的所有改变内部存储结构的操作都会 new 出一个新的 String 对象。
# 字符串拼接用 + 还是 StringBuilder?
Java 语言本身并不支持运算符重载,+
和 +=
是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
字符串对象通过 +
的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
需要注意的是:
如果在循环内使用 +
进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder
以复用,会导致创建过多的 StringBuilder
对象。
即每循环一次就会创建一个 StringBuilder
对象
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {
s += arr[i];
}
System.out.println(s);
2
3
4
5
6
如果直接使用 StringBuilder
对象进行字符串拼接的话,就不会存在这个问题了。
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder(); // 使用 StringBuilder 对象
for (String value : arr) {
s.append(value);
}
System.out.println(s);
2
3
4
5
6
# String#equals() 和 Object#equals() 有何区别?
String
中的equals
方法是被重写过的,比较的是 String 字符串的值是否相等。Object
的equals
方法是比较的对象的内存地址
# 字符串常量池的作用了解吗?
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
# String str = new String("aaa");会创建几个字符串对象?
会创建 1 或 2 个字符串对象。
创建 2 个的情况
- 第一个字符串对象是 "aaa",它是在编译期间就创建好的,存储在字符串常量池中。
- 第二个字符串对象是 new String("aaa"),它是在运行期间通过 new 关键字创建的。这个对象会在堆内存中开辟一个新的空间,用于存储字符串 "aaa" 的拷贝。
创建 1 个的情况
但是,如果字符串常量池中已经存在了一个值为 "aaa" 的字符串,那么在执行 new String("aaa") 时,JVM 会先在字符串常量池中查找是否存在相同值的字符串,如果存在,则直接返回该字符串的引用,不会再创建一个新的对象。这种情况下,只会创建一个字符串对象。
# String.intern() 方法有什么作用?
String.intern()
是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中。
可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
// 在常量池中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# String 类型的变量和常量做 + 运算时发生了什么?
1、字符串不加 final
关键字拼接的情况:
String str1 = "str";
String str2 = "ing";
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
2
3
4
5
6
7
8
str3 == str5
这主要是因为在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。
2、字符串使用 final
关键字之后拼接的情况:
被 final
关键字修改之后的 String
会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。
final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing"; // 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d); // true
2
3
4
5
6
如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。
示例代码(str2
在运行时才能确定其值):
final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing"; // 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d); // false
public static String getStr() {
return "ing";
}
2
3
4
5
6
7
8
9
# toString 和 String.valueOf
toString() 方法和 String.valueOf() 方法在 Java 中用于将对象转换为字符串表示形式。
区别
toString()
方法是一个实例方法,必须通过具体的对象调用。它通常用于自定义类,可以根据需要自定义返回的字符串格式。String.valueOf()
方法是一个静态方法,可以直接通过类名调用。它适用于将各种类型的数据转换为字符串,包括基本数据类型、对象和 null。- 如果对象为 null,
toString()
方法会抛出NullPointerException
异常,而String.valueOf()
方法会返回字符串"null"
。
# 默认的 toString() 方法
- 如果在自定义类中没有重写 toString() 方法,将使用 Object 类中的默认实现。
- 默认 toString() 方法返回一个由
类名、@ 符号和对象的哈希码
组成的字符串,例如:"ClassName@HashCode"
。