一、抽象类(Abstract Class)

1.1 核心概念

抽象类是一种不能被实例化的类,用于定义一个类家族的通用行为和属性,同时声明一些必须由子类实现的抽象方法。抽象类通过abstract关键字定义。

1.2 核心特性

  1. 不能实例化:抽象类无法通过new关键字创建对象

    2.可以包含多种成员

  • 普通成员变量和常量
  • 普通方法(有方法体)
  • 抽象方法(无方法体,用abstract修饰)
  • 构造方法(用于子类初始化)
  • 静态成员(静态变量和静态方法)

   3.子类必须实现所有抽象方法

非抽象子类继承抽象类时,必须重写父类所有抽象方法,否则子类也必须声明为抽象类

   4.访问修饰符

  • 抽象类的访问修饰符可以是publicdefault(包访问)
  • 抽象方法不能用private修饰(子类无法重写)
  • 抽象方法不能用final修饰(矛盾,final 方法不能重写)

1.3 抽象类的使用场景

  1. 抽取类家族的共性:当多个类存在共同属性和方法时,将共性部分提取到抽象类中

  2. 定义部分实现:抽象类可以提供部分方法的具体实现,子类只需实现差异化部分

  3. 作为模板:实现 "模板方法模式",抽象类定义算法骨架,子类实现具体步骤

二、接口(Interface)

2.1 核心概念

接口是一种完全抽象的类型,用于定义类应该遵循的规范(方法签名),但不提供具体实现。接口通过interface关键字定义。

2.2 核心特性

  1. 不能实例化:接口同样无法通过new关键字创建对象

  2. 成员特性(重要!易混淆点):

    • 变量:默认隐式被public static final修饰,必须初始化
    • 方法
      • JDK7 及以前:只能是抽象方法,默认public abstract
      • JDK8 及以后:可以有默认方法(default修饰,有方法体)和静态方法(static修饰)
      • JDK9 及以后:可以有私有方法(private修饰,用于默认方法间复用代码)
  3. 类实现接口:通过implements关键字,一个类可以实现多个接口

    4.接口继承接口:接口可以通过extends关键字继承其他接口,且支持多继承

2.3 接口的使用场景

  1. 定义规范 / 契约:明确类应该实现哪些功能,如RunnableComparable接口

  2. 实现多继承:弥补 Java 类单继承的限制,一个类可以实现多个接口

  3. 解耦:通过接口编程,降低类之间的依赖关系

  4. 标记接口:不包含任何方法,仅用于标识类的某种特性(如SerializableCloneable

三、抽象类与接口的核心区别

特性 抽象类(Abstract Class) 接口(Interface)
关键字 abstract class interface
继承 / 实现 子类通过extends继承,单继承 类通过implements实现,可多实现
构造方法 有构造方法 无构造方法
成员变量 可以有各种类型的变量 只能是public static final常量
方法类型 可以有抽象方法、普通方法、静态方法等 JDK7 及以前:只能是抽象方法JDK8 及以后:可以有抽象方法、默认方法、静态方法JDK9 及以后:增加私有方法
访问修饰符 可以使用publicprotecteddefault 变量默认public static final方法默认public abstract(抽象方法)
多继承 不支持(类只能单继承) 支持(接口可以多继承其他接口)
实例化 不能实例化 不能实例化
设计理念 体现 "is-a" 关系,强调继承和实现 体现 "has-a" 或 "can-do" 关系,强调功能扩展

四、雷区与常见错误

4.1 抽象类的常见陷阱

  1. 混淆抽象类与普通类

    • 错误:试图实例化抽象类
    • 错误:子类继承抽象类却不实现所有抽象方法(除非子类也是抽象类)
  2. 抽象方法修饰符错误

     3.构造方法使用不当

  • 抽象类可以有构造方法,但不能直接调用
  • 子类必须通过super()调用抽象类的构造方法

     4.过度使用抽象类

  • 误区:只要有抽象方法就创建抽象类
  • 正确:当需要包含实例变量和具体方法实现时才使用抽象类

4.2 接口的常见陷阱

  1. 接口方法修饰符冗余或错误

   2.默认方法冲突:当一个类实现多个接口,而这些接口有相同签名的默认方法时,必须显式重写该方法

     3.接口常量的误用

  • 误区:将接口当作常量类使用(仅定义常量,无方法)
  • 问题:这违反了接口的设计初衷,应使用专门的常量类或枚举

     4.过度设计接口

  • 误区:创建大量细粒度接口,导致实现类需要实现过多接口
  • 正确:接口应保持适度粒度,遵循 "单一职责原则"
   5.接口中不能有静态代码块和构造方法:

6.接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class

4.3 抽象类与接口的选择困惑

  1. 错误地用接口定义实体类的继承关系

    2.将所有方法都声明为抽象方法的抽象类

   3.混淆 "is-a" 与 "can-do" 关系
  • "is-a":用抽象类(如 Dog is an Animal)
  • "can-do":用接口(如 Dog can Swim,实现 Swimmable 接口)

五、最佳实践

  1. 优先使用接口
    • 当不需要继承状态(成员变量)时,优先使用接口
    • 接口更灵活,支持多实现
  2. 结合使用抽象类和接口
    • 抽象类提供基础实现
    • 接口扩展功能3.
    3.接口用于定义类型,抽象类用于代码复用
  • 接口定义 "是什么样的类型"
  • 抽象类提供 "可以复用的实现"
    4.使用接口实现回调机制

    5.接口版本兼容:

 在接口中新增方法时,使用default方法提供默认实现,避免破坏现有实现类

总结

抽象类和接口都是 Java 实现抽象编程的重要手段,它们各有侧重:

  • 抽象类强调继承关系和代码复用,适合定义 "is-a" 关系
  • 接口强调规范和功能扩展,适合定义 "can-do" 关系
  • 核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中 不能包含普通方法, 子类必须重写所有的抽象方法.

六、Object类

Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父 类。即所有类的对象都可以使用Object的引用进行接收。 范例:使用Object接收所有类的对象

1.Object类中的部分重要方法

   1.1toString()方法

   请参考前面章节已详细讲述

   1.2equals()方法

  在Java中,==进行比较时:

a.如果==左右两侧是基本类型变量,比较的是变量中值是否相同

b.如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同

c.如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的:

为什么需要重写equals()方法?

Object 类是所有 Java 类的父类,它默认实现的equals()方法如下:

可以看到,默认的equals()方法本质上还是使用==进行比较,也就是比较两个对象的内存地址是否相同。这意味着如果不重写equals()方法,即使两个对象的内容完全相同(属性值都一样),equals()也会返回false,因为它们是不同的对象,内存地址不同。

假设我们有一个Person类:

如果不重写equals()方法:

虽然p1p2的姓名和年龄都相同,但因为没有重写equals(),比较结果还是false

如何正确重写equals()方法?

重写equals()方法应该遵循以下原则:

  1. 自反性:对象必须等于其自身
  2. 对称性:如果 a.equals (b) 为 true,则 b.equals (a) 也应为 true
  3. 传递性:如果 a.equals (b) 和 b.equals (c) 都为 true,则 a.equals (c) 也应为 true
  4. 一致性:多次调用结果应保持一致
  5. 非空性:任何对象都不能等于 null

Person类重写equals()的示例:

现在再进行比较

重要注意事项

  1. 当重写equals()时,通常也需要重写hashCode()方法,否则会影响 HashMap 等集合的正常工作
  2. 对于引用类型的属性(如示例中的 name),也要使用equals()而不是==进行比较
  3. 可以使用 IDE 自动生成规范的equals()hashCode()方法,避免手动编写时出现逻辑错误

总结来说,当我们需要比较两个对象的内容是否相同时,必须重写equals()方法;如果只是比较两个引用是否指向同一个对象,则使用==即可。

   1.3hashcode()方法

在 Java 中,hashCode()方法是 Object 类定义的另一个重要方法,它返回一个 int 类型的哈希值,主要用于支持哈希表(如 HashMap、HashSet 等集合)的高效操作。

为什么需要 hashCode ()?

哈希表的工作原理是通过 "哈希算法" 将对象映射到数组的特定位置,从而实现快速的查找、插入和删除操作。hashCode()方法就是用来计算对象的哈希值,确定对象在哈希表中的存储位置。

hashCode () 与 equals () 的关系

Java 规范中对两者的关系有明确规定:

  1. 如果两个对象通过equals()方法比较相等,则它们的hashCode()必须返回相同的值
  2. 如果两个对象的hashCode()返回不同的值,则它们通过equals()比较一定不相等
  3. 反之,两个对象的hashCode()相同,equals()比较不一定相等(哈希冲突)

为什么重写 equals () 时必须重写 hashCode ()?

如果只重写equals()而不重写hashCode(),会违反上述规范,导致哈希表工作异常。

举个反例:

问题演示:

出现这种情况是因为 p1 和 p2 的hashCode()(默认使用对象地址计算)不同,HashSet 会认为它们是不同的对象,从而都能被添加进去。

如何正确重写 hashCode ()?

重写hashCode()应遵循:

  1. 同一对象多次调用应返回相同值(在对象属性未修改的情况下)
  2. equals()相等的对象,hashCode()必须相等
  3. 尽量让不同的对象产生不同的哈希值(减少哈希冲突)

为 Person 类重写 hashCode () 的示例:

实用建议

  1. 通常可以使用 IDE 自动生成equals()hashCode()方法,更加规范且不易出错
  2. 参与equals()比较的所有属性都应参与hashCode()的计算
  3. 31 是一个常用的乘数,因为它是质数且计算效率高(31*i = (i<<5) - i)

1、hashcode方法用来确定对象在内存中存储的位置是否相同

2、事实上hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的 散列码,进而确定该对象在散列表中的位置。

总之,hashCode()方法主要用于哈希表的高效操作,而它与equals()的约定关系是保证 Java 集合框架正确工作的基础,重写equals()时必须同时重写hashCode()

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐