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并未出现在Shape的permits列表中。
我们日常使用中,使用这种类比较少,它主要用于一些框架,防止继承被盗用。
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,只允许特定类继承的关键字sealed,permits
10)向上转型,允许定义一个父类,为它赋值一个子类型
Person p = new Student ();
11)向上转型通常与多态相关联。首先定义一个父类类型,编程过程中将其指向某个子类实例,执行时就会调用该子类实例的方法了。
12)在子类覆写方法中,如果要调用父类中的被覆写方法,可以用super.xxx();
13)final关键字修饰类、方法、类属性,分别不允许被继承、覆写、初始化后修改;
14)抽象类和抽象方法
- 用abstract修饰,抽象方法在抽象类中只有定义,没有实现。
- 每个继承的类必须覆写抽象方法,否则该子类还是抽象类。
- 抽象类无法实例化。
15)面向抽象编程
创建父类变量,指向子类实例,调用时运行由实际类型指定:
Person p1=new Student(); Person p2=new Teacher();
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性