Object
1、Object 类的常见方法有哪些?
// native方法,子类不能重写(final修饰),用于返回当前运行时对象的Class对象
public final native Class<?> getClass()
//用于返回对象的哈希码(主要使用在哈希表中,比如 JDK中的 HashMap)
public native int hashCode()
//用于比较 2 个对象的内存地址是否相等;
public boolean equals(Object obj)
//native 方法,用于创建并返回当前对象的一份拷贝。
protected native Object clone() throws CloneNotSupportedException
//返回类的名字实例的哈希码的16进制的字符串(建议Object所有的子类都重写这个方法)
public String toString()
//唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。
//如果有多个线程在等待只会任意唤醒一个。
public final native void notify()
//跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void notifyAll()
//暂停线程的执行。
//注意: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 { }
Object是所有Java类的根类
equals(Object obj):比较两个对象是否相等。默认情况下,该方法比较的是对象的引用,但是可以通过重写该方法来比较对象的内容。hashCode():返回对象的哈希码。哈希码通常是一个整数,用于在哈希表中快速查找对象。toString():返回对象的字符串表示。默认情况下,该方法返回的是对象的类名加上对象的哈希码。getClass():返回对象的类的Class对象。Class对象包含了对象的类型信息,可以用于获取对象的类名、访问类的静态变量和方法等。notify()、notifyAll()、wait():这些方法用于实现线程间的通信,可以在多线程编程中使用。clone():返回对象的一个副本。默认情况下,该方法对于大部分对象来说是不可用的,只有实现了Cloneable接口且重写了clone()方法的类才能使用该方法。
- 比较和哈希码方法组:equals(Object obj)、hashCode()
- 对象转化方法组:toString()、getClass()
- 对象操作方法组:clone()、notify()、notifyAll()、wait()、wait(long timeout)、wait(long timeout, int nanos)
在面试中,如果被问到Object有哪些方法,需要能够准确地列出这些方法,并能够简单描述它们的作用和用法。同时还需要注意有些方法是需要进行重写或者实现接口才能使用的。
2、==和equals()的区别
- ==:如果是基本数据类型,比较两个值是否相等;如果是对象,比较两个对象的引用是否相等,指向同一块内存区域
- equals:用于对象之间,比较两个对象的值是否相等。
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
equals:
- 情况 1:类没有重写 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况 2:类重写了 equals() 方法。一般,我们都重写 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
3、hashCode()的作用
hashCode()方法是Object类的一个方法,它用于获取一个对象的哈希码(int整数),也称为散列码,这个哈希码的作用是确定该对象在哈希表中的索引位置。下面是hashCode()方法的作用:
- 用于实现哈希表(具体怎么实现的可以去了解)。
- 用于HashMap、Hashtable等数据结构。Java中的HashMap、Hashtable等数据结构内部也是使用哈希表来存储数据的。当我们向这些存储数据的集合中添加元素时,Java会利用该元素hashCode()方法计算出该元素的哈希值,并以此确定该元素在哈希表中的存储位置。
- 用于提高对象查找速度。散列数据结构(如散列集合)可以利用哈希码来快速查找对象。如果两个对象equals()方法返回true,则它们的hashCode()也必须相同。
总之,hashCode()方法是Java中非常重要的一个方法,它不仅用于实现哈希表、散列集合等数据结构,还可以用于提高对象查找速度,因此在定义对象时,应该重写该方法,保证返回值的唯一性,同时要保证hashCode()方法与equals()方法一起正确实现。
另一个参考答案:
- 散列表或哈希表的查找操作:在将对象加入到散列表或哈希表中时,会先计算对象的 hashCode 值,然后根据该值将其放入到对应的表格中。在查找对象时,则先计算对象的 hashCode 值,然后在该值所对应的表格中查找相关对象。
- 标识对象的唯一性:即使两个对象的值完全相等,它们在内存中的地址也不相同,对象的 hashCode 值也不同。hashCode 值可以看作是描述一个对象的特征或特性,它可以在一定程度上标识一个对象的唯一性。
因此,在开发中,如果需要在散列表或哈希表中存放对象或比较对象是否相等,就需要在自定义类中实现 hashCode() 方法,以便正确地将对象放入到表格中,或正确地比较对象之间的相等性。
4、hashCode()、equals()的关系?
- 如果两个对象相等,则它们必须有相同的哈希码。(反之未必)
- 如果 equals() 方法被重写,一般 hashCode() 方法也需要被重写。(反之亦是,这是为了保证 hashCode() 相等的对象能够正常比较相等性)
拓展:HashSet(Set接口的典型实现)
当向HashSet中加入一个元素时,它需要判断集合中是否已经包含了这个元素,从而避免重复存储。由于这个判断十分的频繁,所以要讲求效率,绝不能采用遍历集合逐个元素进行比较的方式。实际上,HashSet是通过获取对象的哈希码,以及调用对象的equals()方法来解决这个判断问题的。
HashSet首先会调用对象的hashCode()方法获取其哈希码,并通过哈希码确定该对象在集合中存放的位置。假设这个位置之前已经存了一个对象,则HashSet会调用equals()对两个对象进行比较。若相等则说明对象重复,此时不会保存新加的对象。若不等说明对象不重复,但是它们存储的位置发生了碰撞,此时HashSet会采用链式结构在同一位置保存多个对象,即将新加对象链接到原来对象的之后。之后,再有新添加对象也映射到这个位置时,就需要与这个位置中所有的对象进行equals()比较,若均不相等则将其链到最后一个对象之后。
5、为什么要重写hashCode和equals()方法?
面试时可以简单地回答:
在 Java 中,hashCode() 和 equals() 方法是非常重要的方法,它们通常被用于比较对象的相等性和存储在散列表等数据结构中。如果不重写这些方法,Java 将根据对象在内存中的地址来生成 hashCode() 值,并使用“==”运算符来比较对象是否相等,这可能与我们的期望行为不符。因此,当我们使用自定义类的对象作为散列表的键或在比较对象相等性时,需要对 hashCode() 和 equals() 方法进行重写,以确保它们能够正确地定义对象的相等性和散列表的行为。比如,我们需要重写这些方法来根据对象的特定属性或状态来生成 hashCode() 值,并考虑这些属性或状态在比较对象相等性时的影响。同时,我们还需要确保 hashCode() 方法和 equals() 方法遵循 Java 中的约定,比如保持两个相等的对象的 hashCode() 值相等等。 总体而言,重写 hashCode() 和 equals() 方法是为了实现对对象的正确性比较和散列表的正确行为。
6、说说你理解的hashCode()和equals()方法
思路:hashCode和equals方法的含义及作用、hashCode和equals方法之间的关系(另外凑时间的话,可以补充重写hashCode和equals方法的注意事项)
7、在使用HashMap时有重写hashCode和equals方法,是怎么写的?
你有没有重写过hashCode方法?你在使用HashMap时有没有重写hashCode和equals方法?你是怎么写的?
参考答案:
在使用 HashMap 存储自定义对象时,需要重写对象的 hashCode() 和 equals() 方法,以保证在 HashMap 内部使用的哈希算法和基于哈希表的查找能够正确定位和比较对象。下面是对于自定义对象的 hashCode() 和 equals() 方法的基本实现方法:
- hashCode() 方法
hashCode() 方法通常需要考虑以下几点:
- hashCode() 方法的返回值是一个 int 类型,用于表示对象的哈希值。
- 对于相同的对象,hashCode() 方法应该返回相同的值。
- 对于不同的对象,hashCode() 方法应该尽可能返回不同的值,以提高哈希算法的效率。
对于自定义对象,可以考虑使用对象的各个属性进行哈希值的计算,例如:
@Override public int hashCode() { int result = 17; result = 31 * result + this.id; result = 31 * result + (this.name == null ? 0 : >this.name.hashCode()); return result; }其中,17 和 31 依据是经典的哈希算法。这里使用了对象的 id 和 name 字段进行哈希值的计算。
- equals() 方法
equals() 方法通常需要考虑以下几点:
- equals() 方法的返回值是一个 boolean 类型,用于表示对象的比较结果。
- 对象比较时需要考虑 null 和类型不匹配的情况。
- 对于相同的对象,equals() 方法应该返回 true。
- 对于不同的对象,equals() 方法应该返回 false。
对于自定义对象,可以考虑使用对象的各个属性进行比较,例如:
@Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } User user = (User) obj; return id == user.id && Objects.equals(name, user.name); }其中,getClass() 方法用于获取对象的类型,需要和传入的对象进行比较。这里使用了对象的 id 和 name 字段进行比较。 需要注意的是,重写 hashCode() 和 equals() 方法时,需要保证这两个方法的实现逻辑一致。即对于相等的两个对象,它们的 hashCode() 值应该相同,对于不相等的两个对象,它们的 hashCode() 值应该尽量不同。这样才能保证 HashMap 在内部的存储和查询行为表现正常。
两个比较常见的equals重写方法:(1)用instanceof实现重写equals方法;(2)用getClass实现重写equals方法(更安全)
以下是 HashMap 中重写 hashCode() 和 equals() 方法的示例代码,以及如何在 HashMap 中使用自定义对象作为键的示例。
示例代码: 假设有一个自定义对象 Person,其包含两个属性:name 和 age。
public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } }在 Person 类中重写了 equals() 和 hashCode() 方法。equals() 方法根据 name 和 age 来判断两个 Person 是否相等;hashCode() 方法根据 name 和 age 来计算 Person 的哈希值。
下面是使用自定义对象作为键的 HashMap 示例:
public static void main(String[] args) { Person p1 = new Person("Tom", 20); Person p2 = new Person("Jerry", 30); Map<Person, String> map = new HashMap<>(); map.put(p1, "Tom's info"); map.put(p2, "Jerry's info"); String info = map.get(new Person("Tom", 20)); System.out.println(info); // 输出 "Tom's info" }在上述示例代码中,我们创建了两个 Person 对象 p1 和 p2,并将它们作为键插入到一个 HashMap 对象 map 中。之后,我们使用另外一个 Person 对象(即包含相同 name 和 age 的对象)来查询 map 中对应的值,并输出该值。在这个示例中,由于我们重写了 Person 类中的 equals() 方法和 hashCode() 方法,所以 HashMap 可以正确地将同一个 Person 对象的不同实例视为相等,找到正确的值并输出。
8、重写equals和hashCode时要注意什么?
- 如果两个对象的 equals 相同,那么这两个对象的 hashCode 一定相同,但是反过来不一定,只能够说明这两个对象在一个散列存储结构中
- 如果对象的 equals() 被重写,那么对象的 hashCode() 也要重写(若只重写两个方法中的一个,都无无法保证唯一性),反之也是(两个方法必须同时进行重写,保证它们的实现是一致的)。
针对1,解释如下:equals方法用于比较两个对象是否相等,hashCode方法用于计算对象的哈希码。如果两个对象的equals方法返回true,则它们的hashCode方法必须返回相同的值,以确保它们在散列表中的存储位置相同。但是反过来不一定成立,因为两个不同的对象可能具有相同的散列码,这被称为哈希冲突。因此,equals相同时hashCode必须相同,但是反过来不一定。
追问1:为什么重写equals()就一定要重写hashCode()方法?
如果只重写equals方法,不重写hashCode方法,就有可能导致a.equals(b)这个表达式成立,但是hashCode却不同。
那么这个只重写了equals方法的对象,在使用散列集合进行存储的时候,就会出现问题。
因为 散列集合是使用hashCode来计算key的存储位置。如果存储两个完全相同的对象,但是有不同的hashcode,就会导致这两个对象存储在hash表的不同位置。
当我们想要去根据这个对象去获取数据的时候,就会出现一个悖(bei)论。一个完全相同的对象会出现在hash表的两个位置,那么就会破坏大家约定俗成的规则,使得我们在程序过程中会出现一些不可预料的错误。
详见:【Java面试】为什么重写 equals() 就一定要重写 hashCode() 方法
追问2:如果同一个类中的两个对象hashCode相同,但是equals不同会出现什么问题?
答:如果***,那么它们将被散列表等数据结构视为相同的对象,这可能会导致哈希冲突和数据丢失的问题。因此,在重写hashCode方法时,我们应该确保只有在equals方法返回true时才返回相同的hashCode值。
9、对象的相等和引用相等的区别?
对象相等和引用相等的主要区别在于它们比较的内容不同。
- 引用相等只比较两个引用变量是否指向同一个对象。如果两个引用变量指向同一个对象,那么它们是引用相等;否则,它们不相等。在Java中,使用“==”符号进行比较。
- 对象相等比较的是两个对象的内容是否相等。如果两个对象的内容完全相同,那么它们是对象相等;否则,它们不相等。在Java中,通过equals()方法进行比较。
String
1、String类有哪些方法?
char charAt(int index):返回指定索引处的字符;String substring(int beginIndex, int endIndex):从此字符串中截取出一部分子字符串;String[] split(String regex):以指定的规则将此字符串分割成数组;String trim():删除字符串前导和后置的空格;int indexOf(String str):返回子串在此字符串首次出现的索引;int lastIndexOf(String str):返回子串在此字符串最后出现的索引;boolean startsWith(String prefix):判断此字符串是否以指定的前缀开头;boolean endsWith(String suffix):判断此字符串是否以指定的后缀结尾;String toUpperCase():将此字符串中所有的字符大写;String toLowerCase():将此字符串中所有的字符小写;String replaceFirst(String regex, String replacement):用指定字符串替换第一个匹配的子串;String replaceAll(String regex, String replacement):用指定字符串替换所有的匹配的子串。
注意事项:面试时能说出一些常用的方法,表现出对这个类足够的熟悉就可以了。另外,建议你挑几个方法仔细看看源码实现,面试时可以重点说这几个方法。
2、String、StringBuffer、StringBuilder 的区别?
-
String和 StringBuffer/StringBuilder是Java 中两种不同的字符串处理方式,主要的区别在于String是不可变的(immutable)对象,而StringBuffer和StringBuilder则是可变的(mutable)对象。(String 内部的 value 值是 final 修饰的,所以它是不可变类。所以每次修改String的值,都会产生一个新的对象。StringBuffer 和 StringBuilder 是可变类,字符串的变更不会产生新的对象。)
-
String对象一旦被创建,就不可修改,任何的字符串操作都会返回一个新的String对象,这可能导致频繁的对象创建和销毁,影响性能。而 StringBuffer和StringBuilder允许进行修改操作,提供了一种更高效的字符串处理方式。
-
StringBuffer和StringBuilder的主要区别在于线程安全性和性能方面。StringBuffer是线程安全的(每个操作方法都加了 synchronized 同步关键字),所有方法都是同步的,因此可以被多个线程同时访问和修改。而 StringBuilder不是线程安全的,适用于单线程环境下的字符串处理,但是相比于StringBuffer,StringBuilder具有更高的性能。
因此,当字符串处理需要频繁修改时,建议使用StringBuffer或StringBuilder;而当字符串处理不需要修改时,可以使用String。
3、为什么String是不可变的,而StringBuffer和StringBuilder是可变的?
问题1:String为什么不可变
String类,我看过源代码!String对象其实在内部就是一个个字符,存储在一个char数组(value数组)里面的,并且这个char数组(value数组)是被final修饰的。因为数组一旦创建长度不可变。并且被final修饰的引用一旦指向某个对象之后,不可再指向其它对象,所以String是不可变的!
源代码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
- Java 9之前字符串采用char[]数组来保存字符,即
private final char[] value; - Java 9做了改进,采用byte[]数组来保存字符,即
private final byte[] value;
问题2:StringBuffer和StringBuilder为什么是可变的
我看过源代码,StringBuffer/StringBuilder内部实际上是一个char[]数组,这个char[]数组没有被final修饰,StringBuffer和StringBulider的初始化容量应该为16,当存满之后会进行扩容,底层调用了数组拷贝的方法:System.arraycopy()…扩容的,所以StringBuffer/StringBuilder适用于字符串的频繁拼接操作,并且StringBuffer是线程安全的,StringBuilder是非线程安全的。
StringBuffer源代码:
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
**
* Constructs a string buffer with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuffer() {
super(16);
}
StringBuilder源代码:
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
/** use serialVersionUID for interoperability */
static final long serialVersionUID = 4383685877147921099L;
/**
* Constructs a string builder with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuilder() {
super(16);
}
Java8 String类内部采用的是char数组,Java9之后改用byte数组了,但实质上也可以理解为char
4、为什么String要设计成不可变的?(为什么用final修饰)
Java语言之父JamesGosling的回答是,他会更倾向于使用final,因为它能够缓存结果,当你在传参时不需要考虑谁会修改它的值;如果是可变类的话,则有可能需要重新拷贝出来一个新值进行传参,这样在性能上就会有一定的损失。
- 不可变性:用final修饰String可以保证字符串不可被修改,避免程序中被修改而导致的问题。
- 线程安全:由于String不可变,因此多个线程可以同时访问同一个String对象而不会出现竞争条件,从而保证程序的线程安全。
- 编译期优化:使用final修饰的String可以在编译期间对字符串常量进行优化,将字符串常量嵌入到代码中从而提高程序的效率。
- 内存优化:使用final修饰的String可以更好地使用Java中的字符串常量池,节约内存空间,提高程序的性能。(字符串常量池可以在程序运行时节约很多内存空间,因为不同的字符串变量指向相同的字面量时,都是指向字符串常量池中的同一个对象。这样一方面能够节约内存,另一方面也提升了性能。)
既然我们的String是不可变的,它内部还有很多substring,replace,replaceAll这些操作的方法。这些方法好像会改变String对象?怎么解释呢?
其实不是的,我们每次调用replace等方法,其实会在堆内存中创建了一个新的对象。然后其value数组引用指向不同的对象。
5、String创建对象的几种场景?
在Java中,创建字符串有几种方式:
-
使用双引号("")创建字符串字面值:
String str1 = "hello"; -
通过new操作符创建一个字符串对象(用于动态创建字符串):
String str2 = new String("hello");注意:每次使用这种方式创建字符串时,都会有对应的String对象被创建出来。因此,使用这种方式可能会浪费一些内存空间。
-
通过字符数组创建字符串:
char[] charArray = {'h', 'e', 'l', 'l', 'o'}; String str3 = new String(charArray);这种方法将字符数组转换为字符串,可以用于将字符数组中的数据作为字符串使用。
-
使用静态方法valueOf()将其他类型的数据转换成字符串:
int num = 123; String str4 = String.valueOf(num);
注意:Java中的字符串是不可变的对象,即一旦创建了一个字符串对象,在后续使用过程中无法更改字符串对象的值。因此,在使用时应该注意创建字符串的方式和方式带来的性能和内存开销。
6、String最大长度是多少?
String类提供了一个length方法,返回值为int类型,而int的取值上限为2^31 -1。所以理论上String的最大长度为2^31 -1。
String在不同的状态下,具有不同的长度限制。
- 字符串常量长度不能超过65534
- 堆内字符串的长度不超过2^31-1
7、String a = "abc"; 说一下这个过程会创建什么,放在哪里?
参考答案:JVM会使用常量池来管理字符串直接量。在执行这句话时,JVM会先检查常量池中是否已经存有"abc",若没有则将"abc"存入常量池,否则就复用常量池中已有的"abc",将其引用赋值给变量a。
8、new String("abc") 是去了哪里,仅仅是在堆里面吗?
参考答案:在执行这句话时,JVM会先使用常量池来管理字符串直接量,即将"abc"存入常量池。然后再创建一个新的String对象,这个对象会被保存在堆内存中。并且,堆中对象的数据会指向常量池中的直接量。
9、new String(“hello”)创建了几个字符串对象
在Java中,new String("hello")语句实际上创建了两个字符串对象。
第一个对象是字符串常量池中的“hello”字符串常量,它是字符串常量池中的一个实例对象。第二个对象是使用new关键字创建的一个新的字符串对象,它与字符串常量池中的对象不同。
具体来说,当使用new关键字创建一个字符串对象时,它会首先在字符串常量池中查找是否存在与该对象相等的字符串对象,如果存在,则不创建新的对象,而是将引用指向该对象。否则,它会创建一个新的字符串对象,并将其放入字符串常量池中。因此,即使该字符串已存在于字符串常量池中,使用new关键字也会创建一个新的字符串对象。 因此,new String("hello")创建了一个新的字符串对象,并且还提供了对字符串常量池中的“hello”字符串常量的引用。
10、String类可以被继承吗?
不可以,因为String类有final修饰符,而final修饰的类是不能被继承的。如果试图从String类派生出一个子类,则编译器将会抛出一个错误。
如果需要创建一个可变的字符串,可以使用StringBuilder或StringBuffer类,它们从Object类继承而来,而不是从String类继承而来。
10、java对字符串做过哪些优化
- 字符串常量池:Java 为了减少字符串对象的创建,提高字符串的使用效率,引入了字符串常量池机制,即多个相同字符串常量会被放在常量池中,被多个引用共享使用,避免了创建多个相同字符串对象的浪费。
- StringBuilder和StringBuffer类:这两个类都是用来处理字符串拼接的,相比于直接使用“+”连接字符串,它们可以避免频繁创建新的字符串对象。StringBuilder是线程不安全的,而StringBuffer是线程安全的,因此如果需要多线程环境下使用,应该使用StringBuffer。
- 字符串不可变性:Java中的字符串是不可变的,即一旦创建,就不能被修改。这种不可变性保证了字符串在多个线程中的安全性,同时也可以实现字符串常量池机制。
- intern()方法:这个方法用来将字符串对象添加到常量池中,如果常量池中已经存在相同的字符串,则返回常量池中的字符串,从而避免了创建多个相同字符串对象的浪费。
- 字符串长度缓存:Java中的String类中有一个字段用来缓存字符串的长度,避免了每次获取字符串长度时都需要重新计算。
总之,Java对字符串做了很多优化,这些优化可以提高字符串的使用效率,避免了不必要的资源浪费。