equals vs hashCode
如果重写了 equals 方法,就必须重写 hashCode 方法。这是因为这两个方法在某些数据结构(如 HashMap、HashSet 等)中是密切相关的。
当一个对象被存储在一个哈希表中时,哈希表会使用 hashCode 方法来确定对象存储的位置。如果两个对象的 hashCode 相同,它们会被存储在同一个位置(即“桶”)中。这种现象称为哈希碰撞。
Java 的约定要求,如果两个对象通过 equals 方法被认为是相等的(即 a.equals(b) 返回 true),那么这两个对象的 hashCode 方法也必须返回相同的哈希码。这是为了保证在哈希表中能够正确找到和识别相等的对象。
如果不重写 hashCode 方法,使用相等的对象存储在哈希表中时,可能会导致查找、插入和删除操作出现不正确的行为。例如,如果两个对象 a 和 b 被认为相等(a.equals(b) 返回 true),但是它们的哈希码不相同,可能会导致在哈希表中找不到对象 b,从而引发错误。
comparator vs comparable
Comparator 和 Comparable 都是用于对象比较的接口,但它们有不同的用途和实现方式。
Comparable 接口
定义位置:在对象类本身实现。
主要用途:用于定义对象的自然排序(默认排序),即对象自身具有比较的能力。
方法:需要实现 compareTo(T o) 方法。
排序方式:单一的排序方式,只能在类中定义一种排序规则。
Comparator 接口:
定义位置:在外部定义比较逻辑,不需要修改类的代码。
主要用途:用于定义多个排序规则(自定义排序),可以为同一类对象创建不同的排序方式。
方法:需要实现 compare(T o1, T o2) 方法。
排序方式:可以根据不同的需求定义多个比较器。
使用 Comparable 进行排序,假设有一个 Person 类,按年龄排序:
class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
// 实现 Comparable 接口中的 compareTo 方法
@Override
public int compareTo(Person other) {
// 按年龄升序排序
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
public class ComparableExample {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 35));
// 使用自然排序 (按年龄排序)
Collections.sort(people);
System.out.println(people);
}
}
使用 Comparator 进行排序,假设根据姓名排序,而不是年龄,这时候可以使用 Comparator:
class NameComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
// 按名字升序排序
return p1.getName().compareTo(p2.getName());
}
}
public class ComparatorExample {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 35));
// 使用 Comparator 自定义排序(按姓名排序)
Collections.sort(people, new NameComparator());
System.out.println(people);
}
}
extends vs super
在 Java 的泛型中,extends 和 super 是通配符的上下限,用于定义泛型类型的范围。它们允许在处理泛型类型时灵活地指定类型的继承关系,并在类型安全的前提下使用对象。
? extends T 通配符:上界通配符
含义:表示类型是 T 本身或 T 的子类(即 T 或其子类)。
适用场景:当只读泛型对象时可以使用 extends,因为不确定具体类型是 T 还是其子类,但知道对象至少是 T 类型的某个子类。
限制:只能读取,不能写入(除了 null),因为编译器无法确定写入的类型是否安全。
public class ExtendsExample {
public static void printList(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
// 可以传入 Number 的子类列表,例如 Integer、Double
printList(intList); // 输出:1 2 3
printList(doubleList); // 输出:1.1 2.2 3.3
}
}
在这个例子中,? extends Number 允许 List 和 List 都作为参数传递,因为 Integer 和 Double 都是 Number 的子类。只能读取,因为无法确定泛型的具体类型。
? super T 通配符:下界通配符
含义:表示类型是 T 本身或 T 的父类(即 T 或其父类)。
适用场景:当你需要写入泛型对象时使用 super,因为你只需确保可以向泛型集合中写入 T 类型或其子类型的对象。
限制:只能写入 T 类型或其子类,读取时只能返回 Object 类型,因为无法确定具体类型。
public class SuperExample {
public static void addNumbers(List<? super Integer> list) {
list.add(1); // 可以添加 Integer
list.add(2);
// 不能读取为 Integer,只能作为 Object 读取
Object obj = list.get(0);
}
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
// 可以传入 Integer 的父类列表,例如 Number、Object
addNumbers(numList);
addNumbers(objList);
}
}
在这个例子中,? super Integer 允许向列表中添加 Integer 类型的元素,但不能确定从列表中读取的元素的具体类型,读取的结果是 Object 类型,因为列表的元素可能是 Integer 的父类对象。
方法签名
方法签名(Method Signature)指的是用来唯一标识一个方法的部分。方法签名包括方法名和参数列表,但不包括返回类型和修饰符。在 Java 中,方法签名用于区分同一个类中不同的重载方法。
public void calculate(int a, String b) { }
public int calculate(double x, double y) { }
public void calculate(int a, int b) { }
每个 calculate 方法的签名是不同的,即 (int, String), (double, double), 和 (int, int),因此它们是不同的方法,即重载。
尽管返回类型不同,但在方法签名中不包含返回类型,所以不同返回类型不影响方法签名。
方法重载 vs 方法重写
方法重载是在同一个类中定义多个方法名相同但参数列表不同的方法。方法的参数列表可以通过参数类型、参数数量或参数顺序来进行区别。方法重载通常用于实现相似功能的不同版本,特点如下:
- 必须在同一个类中。
- 方法名相同。
- 参数列表不同(参数类型、数量或顺序至少有一个不同)。
- 可以有不同的返回类型,但返回类型不影响重载。
- 与访问修饰符无关,方法的访问权限可以不同。
方法重写是在子类中定义一个与父类方法签名相同的方法。重写的方法可以对父类的方法进行改写或扩展,使其更适合子类的需求。重写通常用于实现多态性,特点如下:
- 必须在子类和父类之间。
- 方法名相同,且参数列表也必须相同。
- 返回类型必须相同,或者是返回类型的子类(自 Java 5 起)。
- 访问权限不能更严格,但可以更宽松。
- 使用 @Override 注解标注,以便编译器进行检查(非必须,但推荐)。
继承 vs 重写
构造方法
- 构造方法不能被子类继承。构造方法是用于创建类实例的,只适用于定义它的类。
- 构造方法不能被重写。在 Java 中,重写只适用于实例方法(非构造方法、非静态方法)。
- 子类可以调用父类的构造方法,但需要使用 super() 语句显式调用父类的构造方法,以初始化继承自父类的部分。
私有方法
- 私有方法不能被子类继承。私有方法只能在定义它的类内部访问,子类对其不可见。
- 严格意义上,私有方法不能被重写,因为它不属于子类的可访问范围。但在子类中定义相同方法名和参数的私有方法只是一个新的定义,不会覆盖父类的私有方法。
静态方法
- 静态方法可以被子类调用,但不会被真正继承。它们属于类本身,不属于类的实例。
- 静态方法不能被重写,但可以在子类中定义一个相同方法名和参数列表的静态方法,这被称为方法隐藏。在这种情况下,调用哪个方法取决于引用的类型,而不是对象的实际类型。
权限修饰符
public(公共访问权限)
- 对所有类可见,包括其他包中的类。
- 可以应用于类、接口、方法和字段等。使用 public 修饰的成员可以被任何其他类访问。
protected(受保护的访问权限)
- 对同一个包内的类以及其他包中的子类可见。
- 常用于父类的字段或方法,允许子类继承和访问。protected 可以应用于字段和方法,但不能用于类(顶层类)。
default(包访问权限,没有修饰符)
- 仅对同一包内的类可见。
- 通常用于包内访问的字段或方法。可以应用于字段、方法、类和接口,不写修饰符即为默认访问权限。
private(私有访问权限)
- 仅对类内部可见。
- 通常用于需要封装的字段或方法。private 修饰的成员不能被其他类访问,甚至同包内的类也不能访问。
接口 vs 抽象类
在 Java 中,接口(interface)和抽象类(abstract class)都有助于实现多态和代码复用,但它们在设计和用途上存在重要区别:
定义与继承关系
- 接口:通过 interface 关键字定义,类可以实现(implements)多个接口,支持多重继承。
- 抽象类:通过 abstract class 关键字定义,类只能继承(extends)一个抽象类,不支持多重继承。
方法与字段的实现
- 接口
- 默认情况下,接口中所有方法都是 public 和 abstract(在 Java 8+ 中可以有 default 和 static 方法,Java 9+ 中支持 private 方法)。
- 接口不能有实例字段,但可以包含 public static final 的常量。
- 抽象类
- 可以包含抽象方法(无方法体)和具体方法(有方法体)。
- 可以定义字段,子类可以继承这些字段。
构造方法
- 接口:没有构造方法,因为接口不能直接被实例化。
- 抽象类:可以有构造方法,供子类调用以初始化继承的字段。
使用场景
- 接口:用来定义行为规范,适合描述一种能力或行为,如 Comparable、Runnable 等接口,表明实现这些接口的类具有某种行为能力。
- 抽象类:用来表达一种通用的父类概念,适合描述事物的通用属性和行为。通常用于多个子类有共同的属性和行为时,以减少重复代码。
性能
- 接口:实现接口方法时可能会有稍微的性能开销,因为 Java 需要动态地查找接口实现的方法。
- 抽象类:方法调用比接口实现略快,因为继承关系在编译时已确定。
内部类
Java 中的内部类是一种将类嵌套在另一个类内部的机制。内部类主要用来将两者逻辑上相关的类放在一起,增强封装性。Java 中的内部类类型有四种:
成员内部类是定义在另一个类中的普通类。它依赖于外部类的实例。
class Outer {
private String message = "Hello from Outer!";
class Inner {
public void display() {
System.out.println(message); // 访问外部类的成员
}
}
}
public class Test {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner(); // 通过外部类对象创建内部类
inner.display();
}
}
静态内部类是用 static 修饰的内部类。静态内部类不依赖于外部类的实例,因此可以直接通过外部类名来访问。静态内部类只能访问外部类的 static 成员。
class Outer {
private static String message = "Hello from Outer!";
static class StaticInner {
public void display() {
System.out.println(message); // 只能访问外部类的静态成员
}
}
}
public class Test {
public static void main(String[] args) {
Outer.StaticInner inner = new Outer.StaticInner(); // 直接通过外部类访问
inner.display();
}
}
局部内部类是在方法或代码块内定义的类,作用范围仅限于该方法或代码块。局部内部类只能访问方法的 final 或有效 final 的局部变量。
class Outer {
public void outerMethod() {
final String message = "Hello from Local Inner!";
class LocalInner {
public void display() {
System.out.println(message); // 访问方法内的final局部变量
}
}
LocalInner inner = new LocalInner();
inner.display();
}
}
public class Test {
public static void main(String[] args) {
Outer outer = new Outer();
outer.outerMethod();
}
}
匿名内部类是一种没有名字的内部类,通常用来简化代码,仅在需要定义一次性行为时使用。匿名内部类只能用来继承一个类或实现一个接口。
interface Greeting {
void sayHello();
}
public class Test {
public static void main(String[] args) {
Greeting greeting = new Greeting() { // 定义匿名内部类
@Override
public void sayHello() {
System.out.println("Hello from Anonymous Inner Class!");
}
};
greeting.sayHello();
}
}
内部类的主要特性和用途
内部类可以隐藏在外部类中,不对外部暴露。
非静态内部类可以直接访问外部类的成员变量和方法。
当两个类关系紧密,且希望控制内部类的访问范围时,可以使用内部类。