
一、抽象类什么时候用抽象类怎么定义抽象类抽象类有什么作用来我们依次好好盘点一下。首先什么时候用抽象类当你定义类的时候你发现这个类不能描述具体的对象那么你就可以把它定义为抽象类比如Animal什么动物、Car什么汽车、Shape什么图形等等。那么抽象类的语法就要用到abstract关键字来修饰。public abstract class Animal { //代码 }抽象类的特性1.抽象类不能被实例化只能通过子类去调用里面的方法。2.抽象类不能被static、final、private修饰因为子类要继承并重写父类里的抽象方法。3.普通子类继承抽象类必须重写父类的抽象方法如果子类也是抽象类子类可以重写父类的抽象方法也可以不写。4.如果一个类里面有抽象方法那么这个类一定是抽象类相反如果一个类是抽象类那么类里面可以有抽象方法也可以没有。5.父债子还。如果抽象类 A 被抽象类 B 继承B 可以不重写 A 的抽象方法但如果普通类 C 继承了 B那么 C 就要重写 A 和 B 的抽象方法如果 C 只继承 A那么只需要重写 A 的抽象方法。一句话顺着继承链往上找重写所有未实现的抽象方法即可不需要管其他链路。6.abstract 方法父类不实现功能只是用来规范其次子类必须重写并完成功能。抽象类的作用1.抽象类可以定义抽象方法把子类必须实现的行为统一约束起来让子类各自去实现不同的细节从而实现代码的规范化和统一管理。2.抽象类可以抽取多个子类的公共属性和方法把公共部分放在父类中子类直接复用减少重复代码提高代码的复用性。3.抽象类可以定义模板方法在父类中把整体流程和骨架定好把某些步骤延迟到子类去实现从而固定算法的整体结构同时允许子类扩展具体步骤。4.抽象类可以体现面向对象的多态性父类引用指向子类对象调用抽象方法时实际执行的是子类的实现让代码更灵活、更易扩展。5.抽象类可以约束子类的设计让团队在开发时遵循统一的规范避免不同的人写出风格不一致、结构混乱的代码。二、接口1. 接口的语法定义用 interface 来定义接口写接口时一般名称加个“I”名称最好是形容词见词知义里面的方法名称要简洁。成员变量前面默认 public static final成员方法前面默认 public abstract。当然接口里面可以实现具体的功能但是方法要被 static 或者 default默认修饰。public interface IShape { (public static final) int SIZE; (public abstract) void text(); public static void add() { //代码实现 } public default void minus() { //代码实现 } }2.implementsimplements 用来联系类和接口相当于用这个关键字让类拥有了接口中定义的方法。一个类可以实现多个接口这正是接口比继承更灵活的地方——因为 Java 只支持单继承一个类只能继承一个父类无法通过继承获得多个父类的行为所以需要 implements 来实现多接口。extends 和 implements 的区别extends 只能继承一个父类也就是单继承而 implements 可以实现多个接口也就是多实现。两者都用于建立类与类、类与接口之间的关系但适用场景和约束不同。一个类如果实现了多个接口那么这个类要实现接口里面所有的抽象方法老版本的 Java 就是说接口内所有的方法重写实现。下面用表格来对比一下两者的区别对比项extends继承implement实现使用场景类与类之间、接口与接口之间类与接口之间关键字extendsimplements数量限制只能继承一个父类单继承可以实现多个接口多实现继承/实现内容可以继承父类的属性和方法包括抽象方法必须实现接口中所有的抽象方法是否可继承实现方法可以直接复用父类已实现的方法接口中 default 方法可以直接复用抽象方法必须重写设计目的体现类之间的“is-a”关系复用公共代码体现类与接口之间的“can-do”关系约定行为规范当一个类上同时存在 extends 和 implements 时先继承再实现。public class Cat extends Animal implements IRunning{ //代码 }一个接口可以继承多个接口。public interface IRunning extends IFlying,ISwimming{ //代码 }接口的几个特性1.接口类型不能实例化对象接口本身没有构造方法无法通过 new 关键字创建实例只能由实现类来创建对象。2.接口内部不能有构造方法和静态代码块因为接口不能实例化构造方法没有意义同时接口也不允许出现静态代码块来初始化成员。3.接口里面不能实现普通方法只能通过实现接口的类来实现但接口中可以定义 static 方法和 default 方法这两种方法可以带有方法体。4.重写接口中方法时不能使用默认的访问权限必须使用 public 修饰因为接口中的方法默认就是 public abstract实现类重写时访问权限只能放大不能缩小。5.接口中的成员变量默认是 public static final即常量接口不能定义普通的实例变量只能定义常量供实现类使用。6.接口支持多继承一个接口可以同时继承多个接口从而组合多个接口的行为规范这也是接口比类更灵活的地方。三.接口中两种比较方法1.重写 compareTo 方法//这里的 Comparable 是 Java 的接口里面的 compareTo 方法是我们要重写实现的public class Student implements Comparable{ private String name; private int age; //构造函数没有返回值名字和类名一致 public Student(String name, int age){ this.name name; this.age age; } //这里重写Arrays的toString方法是为了最后输出好看 public String toString(){ return [ this.name : this.age ]; } public int compareTo(Object o){ //让o向下转型取得o的性质 Student st (Student) o; if (this.age ((Student) o).age){ return -1; } else if (this.age ((Student) o).age) { return 1; } else { return 0; } } //上面这串if语句可以换成 return Integer.compare(((Student) o).age, this.age);这个实质上就是实现的这串代码 }public class Text { public static void main(String[] args) { Student student1 new Student(xiaohei,5); Student student2 new Student(xiaohua,10); System.out.println(student1.compareTo(student2)); } }这个比较方法比较局限主要有以下几个缺点侵入性强需要修改类本身去实现 Comparable 接口如果类来自第三方库或无法修改源码就无法使用这种方式排序。只能定义一种排序规则一个类只能实现一次 compareTo 方法当同一个对象需要按不同属性如先按年龄、再按姓名排序时就无法满足需求。耦合度高排序逻辑写死在业务类内部导致类的职责不单一后续修改排序规则需要改动业务类本身。参数类型不灵活compareTo 接收的是 Object 类型需要手动强转容易在类型转换时抛出 ClassCastException代码也不够安全。2.Comparator 接口Comparator 是 Java 提供的一个函数式接口位于 java.util 包下核心方法是 compare。它把排序规则从业务类中剥离出来单独定义成一个比较器类从而解决 Comparable 侵入性强、只能定义一种排序规则的问题。下面通过一个例子来演示如何使用 Comparator 接口。public class Student { public String name; public int age; //构造函数没有返回值名字和类名一致 public Student(String name, int age){ this.name name; this.age age; } public String toString(){ return [ this.name : this.age ]; } }接下来定义两个比较器一个按年龄排序一个按姓名排序class AgeComparator implements ComparatorStudent{ public int compare(Student o1,Student o2){ return o1.age - o2.age; } } class NameComparator implements ComparatorStudent{ public int compare(Student o1,Student o2){ return o1.name.compareTo(o2.name); } }最后在测试类中创建两个学生对象分别用两个比较器进行比较public class Text { public static void main(String[] args) { Student student1 new Student(xiaohei,5); Student student2 new Student(xiaohua,10); AgeComparator ageComparator new AgeComparator(); NameComparator nameComparator new NameComparator(); ageComparator.compare(student1,student2); nameComparator.compare(student1,student2); } }从上面的例子可以看出Comparator 接口把排序逻辑独立成比较器类同一个 Student 类可以搭配多个不同的比较器按年龄、按姓名等不同规则分别排序互不干扰代码的扩展性和复用性都更好。四.内部类简单来说内部类就是一个类里面定义了另外一个类。内部类分为静态内部类、实例内部类、局部内部类和匿名内部类四种。前两种比较好理解后两种需要重点学习。1.静态内部类和实例内部类静态内部类是用 static 修饰的内部类它不依赖外部类的实例可以直接通过“外部类.内部类”的方式创建对象。静态内部类只能访问外部类的静态成员不能访问外部类的实例成员。public class Outer { private static int num 10; //静态内部类 public static class Inner { public void show() { System.out.println(静态内部类num num); } } } //使用不需要先创建外部类对象 Outer.Inner inner new Outer.Inner(); inner.show();实例内部类没有 static 修饰它必须依赖外部类的实例才能存在创建对象时要先 new 一个外部类对象再通过外部类对象去 new 内部类对象。实例内部类可以访问外部类的所有成员包括私有成员和实例成员。public class Outer { private int num 20; //实例内部类 public class Inner { public void show() { System.out.println(实例内部类num num); } } } //使用必须先创建外部类对象 Outer outer new Outer(); Outer.Inner inner outer.new Inner(); inner.show();2.局部内部类局部内部类是定义在外部类的方法内部的类它的作用范围仅限于所在的方法体内。局部内部类不能被 public、static 修饰也不能被 private、protected 修饰因为它的生命周期只存在于方法执行期间方法结束后局部内部类就不可见了。局部内部类可以访问外部类的所有成员包括私有成员同时它也能访问方法内的局部变量但要求这些局部变量必须是 final 或 effectively final即初始化后不再改变否则编译会报错。public class Outer { private int num 30; public void show() { int local 5; // effectively final可以被局部内部类访问 //局部内部类 class Inner { public void display() { System.out.println(局部内部类num num , local local); } } Inner inner new Inner(); inner.display(); } } //使用只能在方法内部创建对象 Outer outer new Outer(); outer.show();局部内部类的核心特点作用域限定在方法内外部无法直接访问不能加访问修饰符因为它的可见范围已经由方法边界决定适合在某个方法内部临时封装一段逻辑让代码结构更清晰。3.匿名内部类匿名内部类是没有名字的内部类它通常与接口或抽象类搭配使用在创建对象的同时直接实现接口方法或重写抽象方法适合只需要使用一次的临时场景。匿名内部类不能定义构造方法因为它没有类名它本质上是一个继承了某个类或实现了某个接口的匿名子类对象。public class Outer { public void show() { //匿名内部类实现 Runnable 接口 Runnable task new Runnable() { Override public void run() { System.out.println(匿名内部类执行任务); } }; task.run(); } } //使用直接调用外部类方法 Outer outer new Outer(); outer.show();匿名内部类的核心特点没有类名不能定义构造方法必须继承一个父类或实现一个接口只能使用一次适合临时封装逻辑可以访问外部类的成员访问方法内局部变量时同样要求变量是 final 或 effectively final。五.Object类1.toString方法toString 方法用于返回对象的字符串表示它是 Object 类中定义的方法。Object 类中 toString 的默认实现是返回类名十六进制哈希码这样的字符串例如 com.example.Student1a2b3c4d这种格式对阅读没有意义因此在实际开发中通常需要重写 toString 方法按照业务需求返回有意义的字符串比如把对象的姓名、年龄等字段拼接成可读的文本。重写 toString 方法的核心作用一是方便调试打印对象时能直接看到关键字段的值而不是一串无意义的地址二是方便日志输出和日志分析让程序运行时的信息更直观三是方便与字符串拼接当对象参与字符串拼接或直接打印时会自动调用重写后的 toString 方法。下面是一个重写 toString 方法的示例public class Student { private String name; private int age; public Student(String name, int age) { this.name name; this.age age; } //重写 toString 方法返回有意义的字符串 Override public String toString() { return Student{name name , age age }; } } //使用直接打印对象会自动调用重写后的 toString Student student new Student(xiaohei, 5); System.out.println(student); //输出Student{namexiaohei, age5}需要注意的是toString 方法属于 Object 类所有类都继承自 Object因此任何类都可以重写 toString 方法。重写时建议加上 Override 注解方便编译器检查方法签名是否正确。2.equal方法equals 方法用于比较两个对象的内容是否一致它是 Object 类中定义的方法默认实现是比较两个对象的引用地址因此在实际使用中通常需要重写 equals 方法按照业务字段如姓名、年龄来判断两个对象是否相等。 运算符则用于判断基本类型变量的值是否一致或者判断引用类型变量的地址是否一致即是否指向同一个对象。对于基本类型 比较的是数值对于引用类型 比较的是内存地址而不是对象的内容。两者的核心区别equals 默认比较地址重写后可以比较内容 永远只比较地址引用类型或数值基本类型。所以判断两个对象内容是否相同时应该使用重写后的 equals 方法而不是 。3.hashcode方法hashCode 方法用于返回对象的哈希码它是 Object 类中定义的方法返回的是一个 int 类型的整数。哈希码可以理解为对象的数字指纹它根据对象的内容计算得出用于在哈希表如 HashMap、HashSet中快速定位对象从而大幅提升查找效率。hashCode 方法的核心作用一是配合哈希表使用在 HashMap、HashSet 等集合中先通过 hashCode 定位到对应的桶再在桶内用 equals 精确比较从而高效地判断对象是否存在或去重二是与 equals 方法紧密配合Java 规范要求两个对象如果 equals 相等那么它们的 hashCode 必须相等否则哈希表会出现逻辑错误。重写 hashCode 方法时需要注意一是必须与 equals 方法一起重写如果只重写 equals 而不重写 hashCode会导致两个内容相同的对象在哈希表中被当成不同对象二是计算哈希码时要使用与 equals 相同的字段保证 equals 相等的对象哈希码也相等三是尽量使用稳定的字段计算避免使用会变化的值否则对象放入集合后哈希码改变会导致无法正确查找。下面是一个重写 hashCode 方法的示例public class Student { private String name; private int age; public Student(String name, int age) { this.name name; this.age age; } //重写 equals 方法按姓名和年龄判断两个对象是否相等 Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; Student student (Student) o; return age student.age name.equals(student.name); } //重写 hashCode 方法使用与 equals 相同的字段计算 Override public int hashCode() { return Objects.hash(name, age); } } //使用两个内容相同的对象equals 相等且 hashCode 相等 Student s1 new Student(xiaohei, 5); Student s2 new Student(xiaohei, 5); System.out.println(s1.equals(s2)); //输出true System.out.println(s1.hashCode() s2.hashCode()); //输出true需要注意的是hashCode 方法属于 Object 类所有类都继承自 Object因此任何类都可以重写 hashCode 方法。重写时建议加上 Override 注解并遵循equals 相等则 hashCode 必须相等的约定否则在 HashMap、HashSet 等集合中使用时会出现意想不到的 bug。