Java八股(object)

object类的方法

Java Object 类是所有类的超类,默认提供 11 个核心方法,核心用于对象比较、哈希、字符串表示、线程同步等。

所有类都默认继承与Object类,所以可以直接在所有的类中去重写Object类中的默认方法。

equals 方法

默认实现是比较两个对象的内存地址,和== 的效果一样。在实际开发中如果需要按照对象的具体内容进行比较,那么就需要重写equals方法。

1
2
3
4
5
6
7
8
9
10
11
12
public class User{
private int id;
private String name;

@Override
public boolean equals(Object object){
if(this == object) return true;
if(object == null || getClass() != obejct.getClass()) return false;
User user =(User)object;
return id == user.id;
}
}
  • Obejct 想要取出 id 字段,需要先强转成 User 类型。
  • getClass() 默认是 this.getClass()。

hashcode 方法

和 equals 配套的必须重写 hashCode 方法,因为 Java 的约定是如果两个对象 equals 返回 true,它们的 hashCode 必须相等;如果 hashCode 不相等,equals 一定返回 false。

如果只重写 equals 不重写 hashCode,会导致对象在 HashMap HashSet 等集合中无法正确存储。

比如两个 id 相同的 User 对象,重写过的equals()会返回 true,但由于 hashCode 不同,会被当成两个不同元素存入集合。重写示例:

1
2
3
4
@Override
public int hashCode() {
return Integer.hashCode(id);
}

toString 方法

默认返回类名加 @加对象的哈希码十六进制表示,比如 User@1b6d3586,可读性很差。实际开发中重写它是为了返回对象的具体信息,方便日志打印和调试

1
2
3
4
@Override
public String toString() {
return User id: + id + name: + name;
}

getClass 方法

返回对象运行时的实际类对象,和编译时类型可能不同,而且这个方法不能重写·

比如父类 Animal 的子类 Dog,Animal animal = new Dog() ,animal.getClass() 返回的是 Dog 的类对象,常用于反射场景,比如通过 getClass 获取类的属性和方法:

1
2
3
Animal animal = new Dog();
Class<?> clazz = animal.getClass();
System.out.println(clazz.getName()); // 输出Dog

本例子中涉及的Class类,是在运行时描述“某个类型本身”的信息。animal.getClass()返回的是一个对象,需要转成Class才能拿到属于它的本身信息。比如他的类名,他实现的父类和接口、类中的成员方法和成员字段等等。

可以利用 Class 完成反射过程。

Class类中的方法有:

  • getName():拿到类名
  • getSuperclass():拿到父类
  • getInterfaces():拿实现的接口
  • getDeclaredFields():拿到字段
  • getDeclaredMethods():拿到方法
  • getConstructor():拿到构造器
  • newInstance() / getConstructor().newInstance() :通过反射创建对象

clone 方法

clone方法是默认的浅拷贝。如果对象中存在引用数据类型,拷贝对象会和原来的对象共用该引用类型。

在这里我重新搞清楚了浅拷贝的“引用数据类型”共用的意义。
假设,当前存在这样一个类:

1
2
3
4
5
class User implements Cloneable {
int age;
String name;
Double score;
}

那么在将已存在的user1克隆一个user2的时候:

1
User user2 = (User) user1.clone();
  1. 基本类型 int age
    拷贝的是数值
    两个对象完全独立
  2. 引用类型 String name, Double score
    拷贝的是“对象引用地址”
    • 改引用对象内部的数据会影响彼此
    • 但重新赋值引用变量不会共享
1
2
3
4
5
6
User user1 = new User();
user1.age = 18;
user1.name = "Tom";
user1.score = 99.5;

User user2 = (User) user1.clone();

如果改动user2 的age和name:

1
2
user2.age = 20;          // user1.age 还是 18  ✔️
user2.name = "Jerry"; // user1.name 变成 Jerry ❌? 错!

什么时候浅拷贝会“联动修改”?

当用可变对象 mutable object的时候会联动修改,比如List类型的对象。

1
2
3
4
5
class User {
List<String> tags;
}
User u2 = (User) u1.clone();
u2.tags.add("A");

结果在u1.tags中我们也能看到”A”。

对于以下类型的引用类型对象会发生联动修改:

  • StringBuilder
  • CustomObject
  • Array
  • List/Map/Set
  • Object[]
  • 任何自己 new 出来的 mutable 对象

为什么String不会跟着改变

String 确实是引用类型。但是它有两个特点:是引用类型,immutable(不可变)。

String重新赋值的时候实际上是在堆内存中重新开辟了一个空间,然后让clone的对象指向这一块堆内存。

深克隆的三种方法

可以参见文章:

Java八股(关键字)

notify 和 notifyAll 方法

它们都是用于多线程同步的,和 synchronized 配合使用,作用是唤醒等待当前对象锁的线程。notify 是随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,比如生产者消费者模式中,生产者生产完数据后调用 notifyAll,唤醒等待的消费者线程。

wait 方法

wait 方法有三个重载,作用是让当前持有对象锁的线程释放锁并进入等待状态,直到被 notify notifyAll 唤醒或者等待时间到期。同样需要在 synchronized 同步块或方法中使用。

== 与 equals 方法的区别

对字符串类型

对于字符串变量来说,使用”==”和”equals”比较字符串时,其比较方法是不同的。

“==”比较两个变量本身的值,即两个对象在内存中的首地址,**”equals”比较字符串包含内容是否相同**。

对非字符串类型

对于非字符串变量来说,如果没有对equals()进行重写的话,”==” 和 “equals”方法的作用是相同的。

用来比较对象在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象。

hashcode 和 equals 方法的关系

在 Java 中,对于重写 equals 方法的类,通常也需要重写 hashCode 方法,并且需要遵循以下规定:

  • 一致性:如果两个对象使用 equals 方法比较结果为 true,那么它们的 hashCode 值必须相同。也就是说,如果 obj1.equals(obj2) 返回 true,那么 obj1.hashCode() 必须等于 obj2.hashCode()
  • 非一致性:如果两个对象的 hashCode 值相同,它们使用 equals 方法比较的结果不一定为 true。即 obj1.hashCode() == obj2.hashCode() 时,obj1.equals(obj2) 可能为 false,这种情况称为哈希冲突。

hashCode 和 equals 方法是紧密相关的,重写 equals 方法时必须重写 hashCode 方法,以保证在使用哈希表等数据结构时,对象的相等性判断和存储查找操作能够正常工作。而重写 hashCode 方法时,需要确保相等的对象具有相同的哈希码,但相同哈希码的对象不一定相等。

Java 中 String 类的常用方法

  • 获取长度int length():返回字符串的长度(字符个数)。例:"abc".length() → 3
  • 判断内容boolean equals(Object obj):比较两个字符串的内容是否完全相同(区分大小写)。例:"abc".equals("ABC") → false
  • 截取子串:String substring(int beginIndex):从指定索引开始截取到末尾。例:"hello".substring(2) → “llo”
  • 去除空格:String trim():去除字符串首尾的空白字符(空格、制表符等)。例:" abc ".trim() → “abc”
  • 替换内容:String replace(char oldChar, char newChar):替换所有指定字符。例:"aaa".replace('a', 'b') → “bbb”
  • 判断空字符串:boolean isEmpty():判断字符串长度是否为 0(注意:null 调用会报错,需先判空)。

String、StringBuffer、StringBuilder的区别和联系

1、可变性 :String 是不可变的(Immutable),一旦创建,内容无法修改,每次修改都会生成一个新的对象。StringBuilder 和 StringBuffer 是可变的(Mutable),可以直接对字符串内容进行修改而不会创建新对象。

2、线程安全性 :String 因为不可变,天然线程安全。StringBuilder 不是线程安全的,适用于单线程环境。StringBuffer 是线程安全的,其方法通过 synchronized 关键字实现同步,适用于多线程环境。

3、性能 :String 性能最低,尤其是在频繁修改字符串时会生成大量临时对象,增加内存开销和垃圾回收压力。StringBuilder 性能最高,因为它没有线程安全的开销,适合单线程下的字符串操作。StringBuffer 性能略低于 StringBuilder,因为它的线程安全机制引入了同步开销。
4、使用场景 :如果字符串内容固定或不常变化,优先使用 String。如果需要频繁修改字符串且在单线程环境下,使用 StringBuilder。如果需要频繁修改字符串且在多线程环境下,使用 StringBuffer

特性 String StringBuilder StringBuffer
不可变性 不可变 可变 可变
线程安全 是(因不可变) 是(同步方法)
性能 低(频繁修改时) 高(单线程) 中(多线程安全)
适用场景 静态字符串 单线程动态字符串 多线程动态字符串