Java:类、继承、多态、抽象、面向抽象编程(2021.4.26)

1、class

定义

class Person{
    public String name;
    public int age;
}

Java虽然有很多地方与C++相同,但是class的定义这一块却是有所不同——C++类定义结束后,还有个分号,而Java没有。

创建实例

Person ming = new Person();

属性和方法

属性一般设置为private权限,不允许从外部访问;

方法一般设置为public权限,通过方法实现对属性的提取与修改;

复制代码
class Person{
    private String name;
    private int age;
    
    public String getName(){
        return this.name;
    }
    public vod setName(String name){
        this.name=name;
    }
    public int getAge(){
        return this.age;
    }
    public void setAge(int age){
        if(age<0 || age>100){
            throw new IllegalArgumentException("invalid age value");
        }
        this.age=age;
    }
}
复制代码

如果将方法设置为private,也是有帮助的,那样的话在类中方法就可以访问该方法,同时外部又不能访问该方法,在类内访问该方法时,不需要加this.

 

this变量

在方法内部,可以使用一个隐含变量this,它始终指向当前实例。因此,通过this.field就可以访问当前实例的字段了。

应该注意到,我们既然可以在方法中直接通过类属性名访问属性了,为什么还要多此一举加一个this呢?这是因为,在类方法中可能定义了一些与类属性同名局部变量,而局部变量的优先级更高,因此要访问类属性时,就必须加this指明了:

public void setName(String name){
    this.name=name; //第一个name是属性,第二个name是传入参数
}

 

注意:Java中的this与C++中的含义是相同的,都是指向当前实例,但是用法上不同,

Java——this.xxx

C++——this->xxx

Python——self.xxx

2、方法参数

①可变参数

可变参数用类型...定义,使用时相当于数组类型,即调用时可以一次性传入多个(任意数量)该类型的参数

class Group{
    private String [] names;
    
    public void setNames(String... names){
        this.names=names;
    }
}

调用

Group g = new Group();
g.setNames("Leo","Alice","Tom");
g.setNames("Leo","Alice");

为什么不用类型[ ]

将上文例子写为:

public void setNames(String [] names){
    this.names = names;
}

原因①

调用时要自己构造String [] ,比如:

Group g = new Group();
g.setNames(new String[] {"Leo","Alice","Tom"});

此时虽然仍然传入了多个数据,但是实际上仍是传入单个变量——一个String[]数组,且每次都要重新new String[]构造数组,比较麻烦!

原因②

该方式可以接收null作为参数,而使用可变参数则能保证无法传入null

3、构造函数/方法

Java构造函数的写法和C++相同:

class 类名{
    public 类名(参数){
        this.属性1=参数1;
        this.属性2=参数2;
    }
}

如果我们自定义了一个构造函数,那么默认构造函数就会失效,除非我们额外再写一个构造函数。

class Person{
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public Person(){
    }
}

没有在构造函数中对属性进行初始化时,引用字段的默认值是null,其它数值字段都是0。

一个构造函数可以调用其它的构造函数,其目的是方便代码复用——在一个构造函数中,调用其它构造方法的语法是this(...)

复制代码
class Person{
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public Person(String name){
        this(name,18);
    }
    public Person(){
        this('Unnamed');
    }
}
复制代码

4、方法重载

重载:方法名相同,但各自的参数不同

5、继承

①基本概念

关键字:extends

用法

假设类A继承自类B

class B{}

class A extends B{}

结果

子类继承了所有父类的(非private)字段与方法;这样,只需要在子类中写它特有的字段与方法就可以了。

注意

  • 严禁定义与父类重名的字段;
  • 子类中没有继承private字段,自然也就无法访问这些字段

②继承树

基类在定义时,虽然我们没有写extends。但是Java中没有写extends的类,编译器都会自动加上extends Object。因此,除了Object的任何类,都会继承自某个类。

Java只允许一个class继承自一个类,不允许多继承,因此一个类有且仅有一个父类

③protected权限

Java的继承与C++的公有(public)继承是一回事——父类的private字段无法被继承,也无法被访问。

为了让子类可以访问父类的字段,我们可以把private改为protected。用protected修饰的字段可以①无法在外部方法直接访问;②在子类中保留下来;

复制代码
class Person{
    protected String name;
    protected int age;
}

class Student extends Person{
    public String hello(){
        return "Hello , "+name;
    }
}
复制代码

④super

super关键字表示父类。子类引用父类的字段时,可以用super.xxx,其作用类似this.xxx,只不过不是本对象,而是父类的字段内容。

该关键字常用于子类的构造函数中,用于调用父类的构造函数初始化那些父类中的字段。

复制代码
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 grade;
    public Student(String name,int age,int score){
        this.grade=grade;
        super(name,age);
    }
}
复制代码

 

在Java中,任何class的构造函数,第一行语句必须是调用父类的构造方法,如果没有写这句话,编译器会自动帮我们加一句super();

这里还引出了另一个问题,子类不会继承父类的构造函数。子类默认的构造函数是编译器自动生成的,不是继承的。

⑤阻止继承

目的

阻止后续类从该类继承

如何实现

将该类用final修饰;

从Java 15开始,允许使用sealed(封存)修饰class,并通过permits明确写出能够从该class继承的子类名称;

例子

例如,定义一个Shape类,只允许被子类Rect、Circle、Triangle继承

public sealed class Shape permits Rect,Circle,Triangle{
    ...
}

上边的类Shape就是一个sealed类,它只允许继承有三个类Rect、Circle、Triangle。

①如果写

public final class Rect extends Shape{...}

就是正确的继承类Rect,因为它出现在了Shape类的permits列表中,且该类是final类,无法被再次继承了。

②如果写

public final class Ellipse extends Shape{...}

就会报一个编译错误,原因是Ellipse并未出现在Shapepermits列表中。

我们日常使用中,使用这种类比较少,它主要用于一些框架,防止继承被盗用。

sealed在Java15中目前是预览状态,要启用它,必须使用参数--enable-preview--source 15

⑥向上转型

含义

可以用一个父类变量承接一个子类对象,即,赋值号左边是父类类型,右边是一个子类对象:

Person p = new Student();

⑦向下转型

含义

把一个父类类型强制转化为子类类型;

因为子类功能比父类父类多,多的功能无法凭空变出来,所以向下转型很可能会失败,失败时JVM会报ClassCastException

为了避免向下转型失败,Java提供了instanceof操作符,用于判断一个实例是否为某种类型

Person p = new Student();
if(p instanceof Student){
        Student s = (Student) p;
}

从Java 14开始,在判断instanceof进行类型判断之后,可以直接强制类型转换,避免再次写出来,那么上文就可以改写成下边这样:

Person p = new Student();
if(p instanceof Student s){//直接类型判断和向下转型二合一了
    ...
}

不过在使用时,上边这种时常会报错,还是一步一步写为好。

6、多态

①函数覆写

在继承关系中,如果在子类中定义一个与父类方法名完全相同的方法,被称为覆写(Override)——①方法名相同;②参数相同;③返回值相同

例如:某类Person中有方法run,在子类Student中,覆写了该方法:

复制代码
class Person{
    public void run(){
        System.out.println("Person.run");
    }
}

class Student extends Person{
    @Override
    public void run(){
        System.out.println("Student.run");
    }
}
复制代码

Override与Overload不同之处在于,Override是在子类中重写继承来的方法,这两个方法必须同名;Overload则是在同一个类中再写一个同名但不同参数的函数,使用时编译器自动根据参数判断使用的是哪个函数。

加上@Override可以让编辑器检查是否进行了正确的覆写。

 

②多态

针对某个类型的方法调用,其真正执行的方法取决于运行时实际类型的方法。这里有两项值得注意的地方:①运行时;②实际类型;

也就是说,我们编写好的代码,只有实际运行时才知道执行的是哪个类型的方法。可能是父类,可能是子类,这一点在使用时应与之前的向上转型部分相关联。

③覆写Object方法

由于所有class均最终继承自Object,而Object定义了几个重要的方法:

toString():把instance输出为String

equals():判断两个instance是否逻辑相等

hashCode():计算一个instance的哈希值

在必要的情况下,我们可以Overriden这几个方法:

public String toString(){
    return "xxxString";
}

public boolean equals(Object o){}

public int hashCode(){return this.xxx.hashCode();}

④调用super

在子类的覆写方法中,如果要调用父类的覆写方法,可以通过super来调用

@Override
public void run(){
    super.run();
}

⑤final

如果一个父类不允许子类对其某个方法进行覆写,可以把该方法标记为final

final修饰的方法不能被override;用final修饰class不能被继承;用final修饰类属性,该属性在初始化(在构造函数中初始化)后不能被修改

7、抽象类

①抽象方法

由于多态的存在,每个子类都可以覆写父类的方法,但是正常情况下,不管父类方法有没有用,都要有大括号,即

public void run(){}

不能只是单单给出一个定义,像下边这样就是错误的

public void run();

如果你对C++有一定的知识,就会想到C++中像上边这样声明一个方法是正确的,这样声明的函数,叫做虚函数。那么Java中该如何实现呢?答案是对方法加修饰标识符abstract,表明它是一个抽象方法

特点

  • 抽象方法没有语句
  • 抽象方法无法执行
  • 抽象方法所在class无法实例化,该class就是抽象类

②抽象类

定义

使用abstract修饰的类

作用

无法实例化,只能被继承

特点

  • 子类继承抽象类之后,必须覆写抽象方法

③面向抽象编程

正如我们在向上转型部分所说的,可以为一个父类变量赋值一个子类类型,这点在抽象类上也适用。

例如,我们有一抽象类Person,其中有一抽象方法run(),子类Student、Teacher继承自该抽象类,那么,我们可以通过抽象类Person去引用具体子类的实例:

Person s = new Student() ;
Person t = new Teacher();

这样引用抽象类的好处在于,我们对其进行方法调用时,不用关心Person类型变量的具体子类型:

s.run();
t.run();

这种尽量引用抽象父类,避免引用实际子类的方式,称为面向抽象编程。

本质

  • 上层代码(抽象类)定义规范,下层代码(子类)具体实现;
  • 不需要子类就可以实现业务逻辑(正常编译)
  • 具体业务逻辑由不同子类实现,对调用者透明

补充

如果不实现抽象方法,那么该子类仍是一个抽象类。

 

8、总结

1)参数个数不确定时,应使用可变参数,写法是类型...,这样就可以传入任意数量这个类型的变量了(而非直接传入一个数组,当然直接传入一个数组也是可行的,见下一段),区别于Python,Python中的可变参数是指参数数量任意格式随意,即*args

可变参数也可以写为类型 [ ] 参数名的形式,但是在传入时应注意传入一个该类型的数组实例。即需要new 类型 [ ] { 值1,值2,值3,... } 

2)当我们自定义构造函数后,默认的无参构造函数就会失效,除非我们额外再定义一个

3)构造函数中通过this(xxx)调用本类的其他构造函数,如果是继承下来的类,想对父类中继承下来的属性初始化,可以用父类的构造函数,super(xxx)

4)Java中的继承相当于C++中的公有继承,不会继承private修饰的属性和方法;对于既不想被类外直接问,又允许被继承下来的属性,可以用protected修饰

5)继承标识符extends,Java只允许单继承,不允许多继承

6)不要在继承类中定义和父类中同名的字段;

7)在用super标识符时,仍然无法访问父类中的private,可以理解为,父类中的private只能父类自己用

8)方法重写(Override)——方法名相同,参数相同,返回类型相同;方法重载(Overload)——方法名相同,其他的都可以不同

9)阻止继承关键字final,只允许特定类继承的关键字sealedpermits

10)向上转型,允许定义一个父类,为它赋值一个子类型

Person p = new Student ();

11)向上转型通常与多态相关联。首先定义一个父类类型,编程过程中将其指向某个子类实例,执行时就会调用该子类实例的方法了。

12)在子类覆写方法中,如果要调用父类中的被覆写方法,可以用super.xxx()

13)final关键字修饰类、方法、类属性,分别不允许被继承、覆写、初始化后修改

14)抽象类和抽象方法

  • abstract修饰,抽象方法在抽象类中只有定义,没有实现。
  • 每个继承的类必须覆写抽象方法,否则该子类还是抽象类。
  • 抽象类无法实例化。

15)面向抽象编程

创建父类变量指向子类实例,调用时运行由实际类型指定:

Person p1=new Student();
Person p2=new Teacher();

 

posted @   ShineLe  阅读(63)  评论(0编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示