2026/4/15 7:24:09
网站建设
项目流程
明光市建设局网站,wordpress百度主动插件,33岁改行做网站建设,公司注册法人查询一前言
今天不知道哪来的斗志#xff0c;感觉状态在回归#xff0c;所有多写一点#xff0c;这次的内容我感觉是很重要的。
大家一起努力吧。
二主要内容
继承
在前面的章节中#xff0c;我们已经定义了Person类#xff1a;
class Person {private String name;priv…一前言今天不知道哪来的斗志感觉状态在回归所有多写一点这次的内容我感觉是很重要的。大家一起努力吧。二主要内容继承在前面的章节中我们已经定义了Person类class Person { private String name; private int age; public String getName() {...} public void setName(String name) {...} public int getAge() {...} public void setAge(int age) {...} }现在假设需要定义一个Student类字段如下class Student { private String name; private int age; private int score; public String getName() {...} public void setName(String name) {...} public int getAge() {...} public void setAge(int age) {...} public int getScore() { … } public void setScore(int score) { … } }仔细观察发现Student类包含了Person类已有的字段和方法只是多出了一个score字段和相应的getScore()、setScore()方法。能不能在Student中不要写重复的代码这个时候继承就派上用场了。继承是面向对象编程中非常强大的一种机制它首先可以复用代码。当我们让Student从Person继承时Student就获得了Person的所有功能我们只需要为Student编写新增的功能。Java使用extends关键字来实现继承class Person { private String name; private int age; public String getName() {...} public void setName(String name) {...} public int getAge() {...} public void setAge(int age) {...} } class Student extends Person { // 不要重复name和age字段/方法, // 只需要定义新增score字段/方法: private int score; public int getScore() { … } public void setScore(int score) { … } }可见通过继承Student只需要编写额外的功能不再需要重复代码。注意子类自动获得了父类的所有字段严禁定义与父类重名的字段在OOP的术语中我们把Person称为超类super class父类parent class基类base class把Student称为子类subclass扩展类extended class。继承树注意到我们在定义Person的时候没有写extends。在Java中没有明确写extends的类编译器会自动加上extends Object。所以任何类除了Object都会继承自某个类。下图是Person、Student的继承树┌───────────┐ │ Object │ └───────────┘ ▲ │ ┌───────────┐ │ Person │ └───────────┘ ▲ │ ┌───────────┐ │ Student │ └───────────┘Java只允许一个class继承自一个类因此一个类有且仅有一个父类。只有Object特殊它没有父类。类似的如果我们定义一个继承自Person的Teacher它们的继承树关系如下┌───────────┐ │ Object │ └───────────┘ ▲ │ ┌───────────┐ │ Person │ └───────────┘ ▲ ▲ │ │ │ │ ┌───────────┐ ┌───────────┐ │ Student │ │ Teacher │ └───────────┘ └───────────┘protected继承有个特点就是子类无法访问父类的private字段或者private方法。例如Student类就无法访问Person类的name和age字段class Person { private String name; private int age; } class Student extends Person { public String hello() { return Hello, name; // 编译错误无法访问name字段 } }这使得继承的作用被削弱了。为了让子类可以访问父类的字段我们需要把private改为protected。用protected修饰的字段可以被子类访问class Person { protected String name; protected int age; } class Student extends Person { public String hello() { return Hello, name; // OK! } }因此protected关键字可以把字段和方法的访问权限控制在继承树内部一个protected字段和方法可以被其子类以及子类的子类所访问后面我们还会详细讲解。supersuper关键字表示父类超类。子类引用父类的字段时可以用super.fieldName。例如class Student extends Person { public String hello() { return Hello, super.name; } }实际上这里使用super.name或者this.name或者name效果都是一样的。编译器会自动定位到父类的name字段。但是在某些时候就必须使用super。我们来看一个例子// super public class Main { public static void main(String[] args) { Student s new Student(Xiao Ming, 12, 89); } } class Person { protected String name; protected int age; public Person(String name, int age) { this.name name; this.age age; } } class Student extends Person { protected int score; public Student(String name, int age, int score) { this.score score; } }运行上面的代码会得到一个编译错误大意是在Student的构造方法中无法调用Person的构造方法。这是因为在Java中任何class的构造方法第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法编译器会帮我们自动加一句super();所以Student类的构造方法实际上是这样class Student extends Person { protected int score; public Student(String name, int age, int score) { super(); // 自动调用父类的构造方法 this.score score; } }但是Person类并没有无参数的构造方法因此编译失败。解决方法是调用Person类存在的某个构造方法。例如class Student extends Person { protected int score; public Student(String name, int age, int score) { super(name, age); // 调用父类的构造方法Person(String, int) this.score score; } }这样就可以正常编译了因此我们得出结论如果父类没有默认的构造方法子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。这里还顺带引出了另一个问题即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的不是继承的。阻止继承正常情况下只要某个class没有final修饰符那么任何类都可以从该class继承。从Java 15开始允许使用sealed修饰class并通过permits明确写出能够从该class继承的子类名称。例如定义一个Shape类public sealed class Shape permits Rect, Circle, Triangle { ... }上述Shape类就是一个sealed类它只允许指定的3个类继承它。如果写public final class Rect extends Shape {...}是没问题的因为Rect出现在Shape的permits列表中。但是如果定义一个Ellipse就会报错public final class Ellipse extends Shape {...} // Compile error: class is not allowed to extend sealed class: Shape原因是Ellipse并未出现在Shape的permits列表中。这种sealed类主要用于一些框架防止继承被滥用。sealed类在Java 15中目前是预览状态要启用它必须使用参数--enable-preview和--source 15。向上转型如果一个引用变量的类型是Student那么它可以指向一个Student类型的实例Student s new Student();如果一个引用类型的变量是Person那么它可以指向一个Person类型的实例Person p new Person();现在问题来了如果Student是从Person继承下来的那么一个引用类型为Person的变量能否指向Student类型的实例Person p new Student(); // ???测试一下就可以发现这种指向是允许的这是因为Student继承自Person因此它拥有Person的全部功能。Person类型的变量如果指向Student类型的实例对它进行操作是没有问题的这种把一个子类类型安全地变为父类类型的赋值被称为向上转型upcasting。向上转型实际上是把一个子类型安全地变为更加抽象的父类型Student s new Student(); Person p s; // upcasting, ok Object o1 p; // upcasting, ok Object o2 s; // upcasting, ok注意到继承树是Student Person Object所以可以把Student类型转型为Person或者更高层次的Object。向下转型和向上转型相反如果把一个父类类型强制转型为子类类型就是向下转型downcasting。例如Person p1 new Student(); // upcasting, ok Person p2 new Person(); Student s1 (Student) p1; // ok Student s2 (Student) p2; // runtime error! ClassCastException!如果测试上面的代码可以发现Person类型p1实际指向Student实例Person类型变量p2实际指向Person实例。在向下转型的时候把p1转型为Student会成功因为p1确实指向Student实例把p2转型为Student会失败因为p2的实际类型是Person不能把父类变为子类因为子类功能比父类多多的功能无法凭空变出来。因此向下转型很可能会失败。失败的时候Java虚拟机会报ClassCastException。为了避免向下转型出错Java提供了instanceof操作符可以先判断一个实例究竟是不是某种类型Person p new Person(); System.out.println(p instanceof Person); // true System.out.println(p instanceof Student); // false Student s new Student(); System.out.println(s instanceof Person); // true System.out.println(s instanceof Student); // true Student n null; System.out.println(n instanceof Student); // falseinstanceof实际上判断一个变量所指向的实例是否是指定类型或者这个类型的子类。如果一个引用变量为null那么对任何instanceof的判断都为false。利用instanceof在向下转型前可以先判断Person p new Student(); if (p instanceof Student) { // 只有判断成功才会向下转型: Student s (Student) p; // 一定会成功 }从Java 14开始判断instanceof后可以直接转型为指定变量避免再次强制转型。例如对于以下代码Object obj hello; if (obj instanceof String) { String s (String) obj; System.out.println(s.toUpperCase()); }可以改写如下// instanceof variable: public class Main { public static void main(String[] args) { Object obj hello; if (obj instanceof String s) { // 可以直接使用变量s: System.out.println(s.toUpperCase()); } } }这种使用instanceof的写法更加简洁。区分继承和组合在使用继承时我们要注意逻辑一致性。考察下面的Book类class Book { protected String name; public String getName() {...} public void setName(String name) {...} }这个Book类也有name字段那么我们能不能让Student继承自Book呢class Student extends Book { protected int score; }显然从逻辑上讲这是不合理的Student不应该从Book继承而应该从Person继承。究其原因是因为Student是Person的一种它们是is关系而Student并不是Book。实际上Student和Book的关系是has关系。具有has关系不应该使用继承而是使用组合即Student可以持有一个Book实例class Student extends Person { protected Book book; protected int score; }因此继承是is关系组合是has关系。小结继承是面向对象编程的一种强大的代码复用方式Java只允许单继承所有类最终的根类是Objectprotected允许子类访问父类的字段和方法子类的构造方法可以通过super()调用父类的构造方法可以安全地向上转型为更抽象的类型可以强制向下转型最好借助instanceof判断子类和父类的关系是ishas关系不能用继承。多态在继承关系中子类如果定义了一个与父类方法签名完全相同的方法被称为覆写Override。例如在Person类中我们定义了run()方法class Person { public void run() { System.out.println(Person.run); } }在子类Student中覆写这个run()方法class Student extends Person { Override public void run() { System.out.println(Student.run); } }Override和Overload不同的是如果方法签名不同就是OverloadOverload方法是一个新方法如果方法签名相同并且返回值也相同就是Override。注意方法名相同方法参数相同但方法返回值不同也是不同的方法。在Java程序中出现这种情况编译器会报错。class Person { public void run() { … } } class Student extends Person { // 不是Override因为参数不同: public void run(String s) { … } // 不是Override因为返回值不同: public int run() { … } }加上Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写但是不小心写错了方法签名编译器会报错。// override public class Main { public static void main(String[] args) { } } class Person { public void run() {} } public class Student extends Person { Override // Compile error! public void run(String s) {} }但是Override不是必需的。在上一节中我们已经知道引用变量的声明类型可能与其实际类型不符例如Person p new Student();现在我们考虑一种情况如果子类覆写了父类的方法// override public class Main { public static void main(String[] args) { Person p new Student(); p.run(); // 应该打印Person.run还是Student.run? } } class Person { public void run() { System.out.println(Person.run); } } class Student extends Person { Override public void run() { System.out.println(Student.run); } }那么一个实际类型为Student引用类型为Person的变量调用其run()方法调用的是Person还是Student的run()方法运行一下上面的代码就可以知道实际上调用的方法是Student的run()方法。因此可得出结论Java的实例方法调用是基于运行时的实际类型的动态调用而非变量的声明类型。这个非常重要的特性在面向对象编程中称之为多态。它的英文拼写非常复杂Polymorphic。多态多态是指针对某个类型的方法调用其真正执行的方法取决于运行时期实际类型的方法。例如Person p new Student(); p.run(); // 无法确定运行时究竟调用哪个run()方法有同学会说从上面的代码一看就明白肯定调用的是Student的run()方法啊。但是假设我们编写这样一个方法public void runTwice(Person p) { p.run(); p.run(); }它传入的参数类型是Person我们是无法知道传入的参数实际类型究竟是Person还是Student还是Person的其他子类例如Teacher因此也无法确定调用的是不是Person类定义的run()方法。所以多态的特性就是运行期才能动态决定调用的子类方法。对某个类型调用某个方法执行的实际方法可能是某个子类的覆写方法。这种不确定性的方法调用究竟有什么作用我们还是来举例子。假设我们定义一种收入需要给它报税那么先定义一个Income类class Income { protected double income; public double getTax() { return income * 0.1; // 税率10% } }对于工资收入可以减去一个基数那么我们可以从Income派生出SalaryIncome并覆写getTax()class Salary extends Income { Override public double getTax() { if (income 5000) { return 0; } return (income - 5000) * 0.2; } }如果你享受国务院特殊津贴那么按照规定可以全部免税class StateCouncilSpecialAllowance extends Income { Override public double getTax() { return 0; } }现在我们要编写一个报税的财务软件对于一个人的所有收入进行报税可以这么写public double totalTax(Income... incomes) { double total 0; for (Income income: incomes) { total total income.getTax(); } return total; }来试一下// Polymorphic public class Main { public static void main(String[] args) { // 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税: Income[] incomes new Income[] { new Income(3000), new Salary(7500), new StateCouncilSpecialAllowance(15000) }; System.out.println(totalTax(incomes)); } public static double totalTax(Income... incomes) { double total 0; for (Income income: incomes) { total total income.getTax(); } return total; } } class Income { protected double income; public Income(double income) { this.income income; } public double getTax() { return income * 0.1; // 税率10% } } class Salary extends Income { public Salary(double income) { super(income); } Override public double getTax() { if (income 5000) { return 0; } return (income - 5000) * 0.2; } } class StateCouncilSpecialAllowance extends Income { public StateCouncilSpecialAllowance(double income) { super(income); } Override public double getTax() { return 0; } }观察totalTax()方法利用多态totalTax()方法只需要和Income打交道它完全不需要知道Salary和StateCouncilSpecialAllowance的存在就可以正确计算出总的税。如果我们要新增一种稿费收入只需要从Income派生然后正确覆写getTax()方法就可以。把新的类型传入totalTax()不需要修改任何代码。可见多态具有一个非常强大的功能就是允许添加更多类型的子类实现功能扩展却不需要修改基于父类的代码。覆写Object方法因为所有的class最终都继承自Object而Object定义了几个重要的方法toString()把instance输出为Stringequals()判断两个instance是否逻辑相等hashCode()计算一个instance的哈希值。在必要的情况下我们可以覆写Object的这几个方法。例如class Person { ... // 显示更有意义的字符串: Override public String toString() { return Person:name name; } // 比较是否相等: Override public boolean equals(Object o) { // 当且仅当o为Person类型: if (o instanceof Person) { Person p (Person) o; // 并且name字段相同时返回true: return this.name.equals(p.name); } return false; } // 计算hash: Override public int hashCode() { return this.name.hashCode(); } }调用super在子类的覆写方法中如果要调用父类的被覆写的方法可以通过super来调用。例如class Person { protected String name; public String hello() { return Hello, name; } } class Student extends Person { Override public String hello() { // 调用父类的hello()方法: return super.hello() !; } }final继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写可以把该方法标记为final。用final修饰的方法不能被Overrideclass Person { protected String name; public final String hello() { return Hello, name; } } class Student extends Person { // compile error: 不允许覆写 Override public String hello() { } }如果一个类不希望任何其他类继承自它那么可以把这个类本身标记为final。用final修饰的类不能被继承final class Person { protected String name; } // compile error: 不允许继承自Person class Student extends Person { }对于一个类的实例字段同样可以用final修饰。用final修饰的字段在初始化后不能被修改。例如class Person { public final String name Unamed; }对final字段重新赋值会报错Person p new Person(); p.name New Name; // compile error!可以在构造方法中初始化final字段class Person { public final String name; public Person(String name) { this.name name; } }这种方法更为常用因为可以保证实例一旦创建其final字段就不可修改。小结子类可以覆写父类的方法Override覆写在子类中改变了父类方法的行为Java的方法调用总是作用于运行期对象的实际类型这种行为称为多态final修饰符有多种作用final修饰的方法可以阻止被覆写final修饰的class可以阻止被继承final修饰的field必须在创建对象时初始化随后不可修改。三最后一语这两部分的内容还是很难理解的但是好在写的足够详细所以大家可以反复多看几次自己运行一下。面向大海春暖花开。感谢观看共勉