三.继承——节省了共有属性和方法的代码:语法 class Student extends Person
1.继承基础
1.继承首先是面向对象中非常强的一种机制,他首先可以复用代码(name ,age),让我们的获得了Person全部功能和属性,只需要为Student编写新增的功能。获得分数等等
- java使用extends继承
- 注意点:1.Person被称为基类,超类,父类。Student被称为子类,拓展类
- 2.继承树 - 定义Person类时,我们没有加上extends,在java中,没有写extends的类编译器会自动加上extends Object(对象)。所以,任何类,除了Object(对象),都会继承某个类
- Preson和Student的继承树。
- student ——> Person——>Object
- 3.java只允许class继承一个类,而C++可以继承很多类
-
java一个类clas只允许有且只有一个父类。只有特殊的Object特殊,它没有父类,他是所有继承的头部。
-
-
4.继承有个特点:java中所有类都是公有继承。拿到全部属性,但子类没有办法访问父类的private字段和方法。
-
就是Student没有办法访问name和age, Student s1=new Student(); s1.name 报错error,这使得继承的作用大大削弱了。
- 这时我们需要用protect替换private了,这样可以访问了。所以,protect关键字可以把字段和方法全部控制在继承树内部。
class Student extends Person{
public String hello()
{
return "hello, "+super.name;
}
}
2.super关键字和this关键字
**
(1)this.属性或方法,别的不多讲,就拿下面的这个吃苹果的经典故事展开。**
this在C++是指针,是类本身属性地址,java中是当前对象,this可以调用方法,属性,指向对象本身。
为什么eatApple()后面还可以接函数。apple可以用 . 操作符访问,因为人家是引用。但是eatApple方法的返回类型是是类名并且返回的是this相当于结果
public class Apple {
int i = 0;
Apple eatApple(){
i++;
return this;
}
public static void main(String[] args) {
Apple apple = new Apple();
apple.eatApple().eatApple();
}
}
(2)super关键字表示超类(父类)。有父亲构造才有孩子构造。先造出父亲再搞出你
先造出父亲再搞出你
在Java中,任何类的构造方法的第一行语句必须是调用父类的构造方法,是因为Java中的继承关系。当一个类继承自父类时,它会继承父类的属性和方法。在创建子类的对象时,必须先创建父类的对象,并进行初始化。通过调用父类的构造方法来完成父类对象的初始化,然后再对子类对象进行初始化。
子类引用父类字段时,可以用super.fieldName 领域名
实际上super.name, this name, name; 效果都是一样的。编译器会自动定位到父类的name字段
但有些情况必须用super,比如说:
class Person
{
public static void main(String[] args)
{
Student s1=new Student("张三",12,67);
}
}
class Person
{
protected String name;
protected int age;
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类的构造方法实际上必须在Student(){super() }加上 , super(有参数列表),必须加上参数列表,否则编译失败
‘错误写法没有形式参数’
class Student extends Person{
protected int score;
public Student(String name,int age,int score)
{
super(); //'自动调用父类的构造方法',Person(), 爸爸空手来了
this.socre =score;
}
'正确写法:'
class Student extends Person{
protected int score;
public Student(String name,int age,int score)
{
super(name ,age);//自动调用父类的构造方法Person(Sting,int),爸爸带刀来了
this.socre =score;
}
所以我们得出结论:如果父类没有默认构造方法,子类就必须显示调用super()并给出参数以便以编译器定位到父类的一个合适的构造方法。
这里顺便引出了结论:即子类不会继承父类的任何构造方法。子类默认的构造方法student()是编译器自动生成的,而不是继承的!!!
class Person
{
private String name;
private int age;
public void setName(String name){}
public String getName(){}
}
class Student extends Person{
private int score;
//Student获得了Person全部属性
public void setScore(){}
public void getScore(){}
}
3.方法重写
方法的重写与重载虽然名字很类似,但是确是完全不一样的东西,重写描述是子类和父类的东西。重载必须在同一个类中。
不同点:重写必须全部方法签名和方法返回值类型一模一样。
@Override注解可写可不写!!!
4.阻止继承(难点):
关键字:final, sealed,static
final——最后的,固定的,不可以改变的,java中被誉为最终变量,最终方法,最终类——起到限制作用,俗称:铁公鸡——一毛不拔!!!一点也不给机会
最终变量(Final Variables): 在Java和C#中,使用"final"关键字声明的变量被称为最终变量或常量。
最终变量只能被赋值一次,一旦赋值后就不能再改变其值。
最终方法(Methods): 使用"final"关键字声明的方法不能被子类重写(override)。
这意味着如果你在一个父类中声明了一个方法为"final",那么在任何继承这个父类的子类中都不能有与之同名且参数列表相同的方法。
最终类(Final Classes): 当一个类被声明为"final"时,它不能被其他类继承。
这种限制可以用来保护类的设计和实现细节,防止被不适当或意外的修改。
总的来说,"final"关键字主要用于限制变量的可变性、方法的可重写性和类的可继承性,以增强代码的稳定性和可预测性。
sealed——有封条的,密封的,permits(允许)他们两一般同时用 ——封条其实可以被公家撕下来,也可以被无赖撕下来,permits就是屁股后面跟着的就是“特殊人群”。特殊人群特殊对待!!!
"sealed"关键字主要用于限制类或接口的继承性,以增强代码的控制力和可预测性。在某些情况下,它可以提高编译时的安全性和性能优化。
sealed class Shape permits Rect,Circle.。。。只允许跟着屁股后面的东西继承**
正常情况下,只要某个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。
1.sealed 密封的,不能被继承和重写
2.final 最后的,相当于C++中const不可以改 属性不可以改
static ——静态的,停止的 ,共享,在堆区开辟,static面前众生平等,堆区,这片区域很安静,来来往往方法和属性都必须带有static的标签老老实实。俗称:校长
静态成员变量(Static Member Variables): 静态变量是属于类的,而不是属于类的实例。
当类被加载时,静态变量就会被创建并分配内存。 所有类的实例共享同一个静态变量的值,也就是说,无论创建多少个对象,静态变量只有一份拷贝。
静态变量可以通过类名直接访问,不需要创建类的实例。静态方法(Static Methods): 静态方法也是属于类的,而不是属于类的实例。
静态方法可以在没有类的实例的情况下被调用,同样通过类名直接访问。
静态方法不能访问非静态成员变量或非静态方法,因为它们在没有对象实例的情况下无法确定。静态块(Static Blocks): 静态块是在类加载时执行的一段代码,通常用于初始化静态变量或者执行一些必要的设置操作。
静态内部类(Static Nested Classes): 静态内部类与非静态内部类的主要区别在于,它不需要对外部类的实例进行引用。
静态内部类可以直接通过外部类名加"."来访问。静态导包(Static Import): 在Java中,静态导入允许程序员在不使用类名的情况下直接访问静态成员(如静态变量和静态方法)。
这可以减少代码中的冗余,并提高可读性。
总的来说,"static"关键字主要用于表示那些与类的实例无关,而是与类本身相关的属性和行为。这些静态元素在类的生命周期中有着特殊的地位和作用。
1.静态方法只能访问静态方法和属性。
2. 静态方法和静态成员变量可以直接有类名.静态方法(静态变量)的方式调用。并且还会引入方法重写 的概念!!!
3.静态方法可以被子类继承或重载,但是不可以被子类重写
4.如果子类中的静态方法和父类的静态方法名,参数,返回值类型都一样,这是被允许的,属于再次声明。
public class Test{
public static int func(int num)
{
return num;
}
}
public test extends Test
{
public int func(int num)
{
return num+2;
}
//报错了func也必须是 static
}
static 除了修饰属性和方法外,还有的功能:
静态代码块
可用于类的初始化操作。进而提升程序的性能
由于静态代码块随着类的加载而执行,因此,很多时候会将只需要进行一次的初始化操作放在 static 代 码块中进行。**
5.向上转型,向下转型——人往高处走,水往低处流。
个人总结:类可以看成一种复杂的数据类型,对象看成普通的引用变量,向上转型成功的原因更可能是因为对象拥有两种数据类型,
对象拥有两种数据类型他可以随心所欲了。因为数据类型决定了你可以拥有多大的活动(内存)空间,和你每走一步能走多远(运算,数据操作)
向上转型——铁定成功!!!
如果一个引用变量的类型是Student,那么它可以指向一个Student类型的实例:
Student s = new Student();
如果一个引用类型的变量是Person,那么它可以指向一个Person类型的实例:
Person p = new Person();
现在问题来了:如果Student是从Person继承下来的,那么,一个引用类型为Person的变量,能否指向Student类型的实例?
Person p = new Student(); // ???
多态性(如果加上方法重写就是有多态性,p有了两重身份,他本身数据类型就是Person 但是指向类型确是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实例(P1有双重身份),把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); // false
instanceof实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为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());
}
区分继承和组合——is和has逻辑性
在使用继承时,我们要注意逻辑一致性。
考察下面的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关系。