进阶
Java 中的 this
- this 如果在实例方法或非 static 的方法内部使用,表示调用该方法的对象
- 在构造器内部使用,表示该构造器正在初始化的对象
- this 可以调用一下结构:成员变量、成员方法、构造器
实例方法或构造器中使用当前对象的成员变量、成员方法
在实例方法或构造器中,如果使用当前类的成员变量或成员方法可以在其前面添加 this,增强程序的可读性。
不过,通常我们都习惯省略 this。
但是,当形参与成员变量同名时,并且在方法内或构造器内需要使用成员变量,必须添加 this 来表明该变量是类的成员变量。 即:我们可以用 this 来区分“成员变量”和“局部变量”。
使用 this 访问属性和方法时,如果在本类中未找到,会继续从父类中查找。
举例 1:
package javacode.oop2;
public class TestThis1 { // 定义 TestThis1 类
private String name ;
private int age ;
public static void main(String[] args) {
TestThis1 testThis1 = new TestThis1("小明", 19);
}
public TestThis1(String name,int age){
this.name = name ;
this.age = age ;
}
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public void getInfo(){
System.out.println("姓名:" + name) ;
this.speak();
}
public void speak(){
System.out.println("年龄:" + this.age);
}
}举例 2:
package javacode.oop2;
public class TestThis2 {
int length;
int width;
public int area() {
return this.length * this.width;
}
public int perimeter(){
return 2 * (this.length + this.width);
}
public void print(char sign) {
for (int i = 1; i <= this.width; i++) {
for (int j = 1; j <= this.length; j++) {
System.out.print(sign);
}
System.out.println();
}
}
public String getInfo(){
return "长:" + this.length + ",宽:" + this.width +",面积:" + this.area() +",周长:" + this.perimeter();
}
}
class TestRectangle {
public static void main(String[] args) {
TestThis2 r1 = new TestThis2();
TestThis2 r2 = new TestThis2();
System.out.println("r1 对象:" + r1.getInfo());
System.out.println("r2 对象:" + r2.getInfo());
r1.length = 10;
r1.width = 2;
System.out.println("r1 对象:" + r1.getInfo());
System.out.println("r2 对象:" + r2.getInfo());
r1.print('#');
System.out.println("---------------------");
r1.print('&');
System.out.println("---------------------");
r2.print('#');
System.out.println("---------------------");
r2.print('%');
}
}调用类中的构造器
this() 语法可以调用类中的构造器。
this() 调用的是本类中的无参构造器。
this(参数列表) 调用的是本类中的有参构造器。
package javacode.oop2;
public class TestThis3 {
private String name;
private int age;
// 无参构造
public TestThis3() {
// this("",18);//调用本类有参构造器
}
// 有参构造
public TestThis3(String name) {
this();//调用本类无参构造器
this.name = name;
}
// 有参构造
public TestThis3(String name, int age) {
this(name);//调用本类中有一个 String 参数的构造器
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getInfo() {
return "姓名:" + name + ",年龄:" + age;
}
}注意
不能出现递归调用。比如,调用自身构造器。
如果一个类中声明了 n 个构造器,则最多有 n - 1 个构造器中使用了 this(形参列表)。
this() 和 this(实参列表) 只能声明在构造器首行。
在类的一个构造器中,最多只能声明一个 this(参数列表)。
面向对象的特征之一:继承
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中, 那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成继承关系。
父类在上,子类在下
Person 类。
class Person {
public String name;
public int age;
public Date birthDate;
public String getInfo() {
}
}Student 类。
class Student {
public String name;
public int age;
public Date birthDate;
public String school;
}通过继承,可以简化 Student 类的定义。
class Student extends Person {
public string school;
}TIP
Student 类继承了父类 Person 的所有属性和方法,并增加了一个属性 school。
Student 可以使用 Person 类中所有的属性和方法。
继承的语法
通过 extends 关键字,可以声明一个类 B 继承另外一个类 A,定义格式如下:
[修饰符] class 类A {
...
}
[修饰符] class 类B extends 类A {
...
}细节说明
类可以理解为是一些具有相同特性的事物的抽象描述,这个类可以构造许多不同的实例。
类也可以理解为某一个事物的抽象描述,它只需要实例化一次。
- 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存。
- 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会在它的父类甚至父类的父类找这个方法, 遵循从下往上找的顺序,找到了就停止。如果到根父类都没有找到,就会报编译错误。
子类不能直接访问父类中私有的(private)的成员变量和方法
子类虽会继承父类私有的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承的 get/set 方法进行访问。
在 Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展”
子类在继承父类以后,还可以定义自己特有的方法,这就可以看做是对父类功能上的扩展。
Java 支持多层继承
class A{}
class B extends A{}
class C extends B{}TIP
- 子类和父类是一种相对的概念
- 顶层父类是 Object 类。所有的类默认继承 Object,并将其作为父类。
Java 只支持单继承,不支持多重继承
class C extends B{} // ok
class C extends A,B {} // error练习 1
定义一个学生类 Student,它继承自 Person 类。
package javacode.oop2;
public class TestExtends1 {
}
class Person {
String name;
char sex;
int age;
Person(String name, char sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
@Override
public String toString() {
return "我的名字是" + this.name + "。我的性别是" + this.sex + "。我今年 " + this.age + " 岁了。";
}
}
class Student extends Person {
long id;
int math;
int english;
int computer;
Student(String name, char sex, int age, long id, int math, int english, int computer) {
super(name, sex, age);
this.id = id;
this.math = math;
this.english = english;
this.computer = computer;
}
public double aver() {
return (double) (math + english + computer) / 3;
}
public int max() {
return Math.max(Math.max(math, english), computer);
}
public int min() {
return Math.min(Math.min(math, english), computer);
}
}练习 2
package javacode.oop2;
public class TestExtends2 {
public static void main(String[] args) {
Kids kids = new Kids();
kids.sex = 1;
kids.salary = 2000;
kids.yearsOlb = 29;
kids.printAge();
kids.manOrWoman();
kids.employeed();
}
}
class ManKind {
int sex;
int salary;
void manOrWoman() {
System.out.println(sex == 1 ? "man" : "woman");
}
void employeed() {
System.out.println(salary == 0 ? "no job" : "job");
}
}
class Kids extends ManKind {
int yearsOlb;
void printAge() {
System.out.println("我今年 " + yearsOlb + " 岁了");
}
}练习 3
package javacode.oop2;
public class TestExtends3 {
public static void main(String[] args) {
Cylinder cylinder = new Cylinder();
System.out.println("体积为 " + cylinder.findVolume());
}
}
// 圆类
class Circle {
private double radius;
Circle() {
radius = 1;
}
void setRadius(double radius) {
this.radius = radius;
}
double getRadius() {
return radius;
}
// 计算圆的面积
double findArea() {
return Math.PI * radius * radius;
}
}
// 圆柱类
class Cylinder extends Circle{
// 高
private double length;
Cylinder() {
length = 1;
}
void setLength(double length) {
this.length = length;
}
double getLength() {
return length;
}
double findVolume() {
return this.findArea() * length;
}
}方法的重写
父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于自己当前的类,该怎么办呢? 子类可以对从父类中继承来的方法进行改造,我们称为方法的重写(override、overwrite)。也称为方法的重置、覆盖。
在程序执行时,子类的方法将覆盖父类的方法。
举例
package javacode.oop2;
public class TestOverride1 {
public static void main(String[] args) {
// 创建子类对象
SmartPhone sp = new SmartPhone();
// 调用父类继承而来的方法
sp.call();
// 调用子类重写的方法
sp.showNum();
}
}
// 多约束类型可以赋值给少约束类型,但是少约束类型不能赋值给多约束类型
// 少约束类型
class Person1 {}
// 多约数类型
class Student1 extends Person1 {
int a;
}
class Phone {
Student1 anyValue;
public void sendMessage() {
System.out.println("发短信");
}
public void call() {
System.out.println("打电话");
}
public void showNum() {
System.out.println("来电显示号码");
}
protected Student1 getAny() {
return anyValue;
}
}
class SmartPhone extends Phone {
Student1 longValue;
//重写父类的来电显示功能的方法
@Override
public void showNum() {
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
}
//重写父类的通话功能的方法
@Override
public void call() {
System.out.println("语音通话 或 视频通话");
}
// 将多约束类型赋值给少约束类型是错误的
// public Person1 getAny() {
// return new Person1();
// }
protected Student1 getAny() {
return new Student1();
}
}@Override 的作用
写在方法上面,用来检测是不是满足重写方法的要求。 这个注解就算不写,也不会报错。
建议写上,这样编译器可以帮助我们检查格式,另外也可以让人清晰的知道这是一个重写的方法。
重写方法的要求
- 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表
- 如果返回值类型是基本数据类型和 void,那么返回值类型必须相同。 子类重写的方法的返回值类型与父类被重写的方法的返回值类型相比, 如果是引用数据类型,那么子类不能是少约束类型。因为少约束类型不能赋值给多约束类型,但是多约束类型可以赋值给少约束类型
- 子类重写的方法使用的可访问范围必须大于父类被重写的方法的可访问范围。不允许重写方法的可访问范围越来越小了。 但需要注意父类私有方法不能重写,跨包的父类缺省的方法也不能被重写
- 子类方法抛出的异常的约束不能多于父类被重写方法的异常约束
- 不能重写带有 static 修饰符的方法。因为 static 方法是属于类的
方法重载与重写的区别
方法的重载:方法名相同,形参列表不同,不对返回值类型作限制。
再谈封装性中的 4 种权限修饰
权限修饰符以可访问范围的大小为标准,从小到大排序为:private、缺省、protected、public。
TIP
外部类如果要跨包使用,类的修饰符必须是 public,否则仅限于本包使用
private
可以修饰变量、方法。
类的 private 属性、方法只能在这个类本身的内部使用,该类的实例无法访问,子类的实例也无法访问。
被 private 修饰的静态属性也不能被访问。
package javacode.oop2;
public class PrivateTest1 {
public static void main(String[] args) {
PrivateClass privateClass = new PrivateClass();
// 访问不了 private 属性,飘红
// System.out.println(privateClass.name);
// 访问不了 private 属性,飘红
// System.out.println(privateClass.age);
privateClass.getAge(); // ok
privateClass.getName(); // ok
// 被 private 修饰的静态属性也不能被访问
// privateClass.height;
}
}
class PrivateClass {
private static int height;
private String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
}缺省
如果在同一个包下,缺省类的成员变量、方法可以被该类的实例访问到,也可以被子类访问到。
package javacode.oop2;
public class DefaultTest1 {
String name;
public static void main(String[] args) {
DefaultClass defaultClass = new DefaultClass();
// 下面三行都能正常打印。
// DefaultClass 类的实例可以访问这些省略修饰符的变量、方法
System.out.println(defaultClass.name);
System.out.println(defaultClass.age);
System.out.println(defaultClass.height);
// 继承 DefaultClass 的类,也就是 DefaultClass 子类也可以访问这些变量和方法
DefaultClassSon defaultClassSon = new DefaultClassSon();
System.out.println(defaultClassSon.name);
System.out.println(defaultClassSon.age);
System.out.println(defaultClassSon.height);
}
}
// 具有省略修饰符的变量、方法
class DefaultClass {
String name;
int age;
int height;
}
class DefaultClassSon extends DefaultClass {
String getName() {
return name;
}
}如果是在不同包下呢?
在 package javacode.oop1; 其它包中,该类的实例访问不到缺省修饰符的成员变量。 子类内部也访问不到缺省修饰符的成员变量。
package javacode.oop1;
import javacode.oop2.DefaultTest1;
public class TestDefaultP {
public static void main(String[] args) {
DefaultTest1 defaultTest1 = new DefaultTest1();
// 飘红
// defaultTest1.name
}
}
class TestDD extends DefaultTest1 {
// getName() {
// return name; // no,子类内部也访问不到
// }
}protected
在相同的包下,该类的用 protected 修饰的成员变量可以被该类实例访问到。
package javacode.oop2;
/**
* Description:
*/
public class ProtectedTest1 {
protected String name; // [protected 修饰符]
protected int age; // [protected 修饰符]
protected int height; // [protected 修饰符]
public static void main(String[] args) {
ProtectedTt protectedTt = new ProtectedTt();
// 可以访问到成员变量 name
System.out.println(protectedTt.name);
}
}
class ProtectedTt {
protected String name;
}在不同的包下呢?
其它包中,该类的实例访问不到缺省修饰符的成员变量。但是子类内部可以访问到 protected 修饰的成员。
package javacode.oop1;
import javacode.oop2.ProtectedTest1;
/**
* Description:
*/
public class ProtectedTest2 {
public static void main(String[] args) {
ProtectedTest1 protectedTest1 = new ProtectedTest1();
// 获取不到下面这三个被 protected 修饰的成员属性
// protectedTest1.name
// protectedTest1.age
// protectedTest1.height
TestProtectedSon testProtectedSon = new TestProtectedSon();
// 像这样使用 `对象。成员变量` 的形式,获取不到 protected 的 name
// testProtectedSon.name // no
// 通过 getName 方法拿到 protected 的 name
testProtectedSon.getName();
}
}
class TestProtectedSon extends ProtectedTest1 {
String getName() {
// 只可以在继承子类的内部获取父类 protected 修饰的成员
return name; // ok
}
}public
public 不管是在相同包中,还是在不同包中,该类的实例和类内部可以直接访问到 public 修饰的成员。
关键字 super
简单说明
在 Java 类中使用 super 来调用父类中的指定操作。
- super 可用于访问父类中定义的属性
- super 可用于调用父类中定义的成员方法或者调用父类的构造器。当子父类出现同名成员时,可以用 super 表明调用的是父类中的成员
- super 的追溯不仅限于直接父类,还包括父类的父类
- super 代表父类的内存空间的标识,this 代表本类对象的引用
子类中调用父类被重写的方法
如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法。
如果子类重写了父类的方法,在子类中需要通过 super.方法名 才能调用父类被重写的方法,否则默认调用的子类重写的方法。
package javacode.oop2;
public class SuperTest {
}
class Phone11 {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
// smartphone:智能手机
class SmartPhone11 extends Phone11{
// 重写父类的来电显示功能的方法
public void showNum(){
// 来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
// 保留父类来电显示号码的功能
// 此处必须加 `super.`,否则就是无限递归,那么就会栈内存溢出
super.showNum();
}
}总结
方法前面没有 super. 和 this.,先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯。
方法前面有 this.,先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯。
方法前面有 super.,从当前子类的直接父类找,如果没有,继续往上追溯。
子类中调用父类中同名的成员变量
- 如果实例变量与局部变量重名,可以在实例变量前面加
this.进行区别 - 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见, 在子类中要访问父类声明的实例变量需要在父类实例变量前加
super.,否则默认访问的是子类自己声明的实例变量 - 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量, 也可以用
this.实例访问,也可以用super.实例变量访问
比如:
package javacode.oop2;
public class SuperTest2 {
}
class Father{
int a = 10;
int b = 11;
}
class Son extends Father{
int a = 20;
public void test(){
// 子类与父类的属性同名,子类对象中就有两个 a
System.out.println("子类的 a:" + a); // 20 先找局部变量找,没有再从本类成员变量找
System.out.println("子类的 a:" + this.a); // 20 先从本类成员变量找
System.out.println("父类的 a:" + super.a); // 10 直接从父类成员变量找
// 子类与父类的属性不同名,是同一个 b
System.out.println("b = " + b); // 11 先找局部变量找,没有再从本类成员变量找,没有再从父类找
System.out.println("b = " + this.b); // 11 先从本类成员变量找,没有再从父类找
System.out.println("b = " + super.b); // 11 直接从父类局部变量找
}
public void method(int a, int b){
// 子类与父类的属性同名,子类对象中就有两个成员变量 a,此时方法中还有一个局部变量 a
System.out.println("局部变量的 a:" + a); // 30 先找局部变量
System.out.println("子类的 a:" + this.a); // 20 先从本类成员变量找
System.out.println("父类的 a:" + super.a); // 10 直接从父类成员变量找
System.out.println("b = " + b); // 13 先找局部变量
System.out.println("b = " + this.b); // 11 先从本类成员变量找
System.out.println("b = " + super.b); // 11 直接从父类局部变量找
}
}
class Test{
public static void main(String[] args){
Son son = new Son();
son.test();
son.method(30,13);
}
}总结:就近原则
- 变量前面没有
super.和this.- 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的局部变量
- 如果不是局部变量,先从当前执行代码的本类去找成员变量
- 如果从当前执行代码的本类中没有找到,会往上找父类声明的成员变量(如果权限修饰符允许)
- 变量前面有
this.- 通过 this 找成员变量时,先从当前执行代码的本类去找成员变量
- 如果从当前执行代码的本类中没有找到,会往上找父类声明的成员变量(如果权限修饰符允许)
- 变量前面
super.- 通过 super 找成员变量,直接从当前执行代码的直接父类去找成员变量(如果权限修饰符允许)
- 如果直接父类没有,就去父类的父类中找
子类构造器中调用父类构造器
- 子类继承父类时,不会继承父类的构造器。只能通过
super(形参列表)的方式调用父类指定的构造器 super(形参列表)必须声明在构造器的首行- 在构造器的首行也可以使用
this(形参列表),调用本类中重载的构造器。 在构造器的首行,this(形参列表)和super(形参列表)只能二选一 - 如果在子类构造器的首行既没有显示调用
this(形参列表),也没有显式调用super(形参列表), 则子类此构造器默认调用super(),即调用父类中空参的构造器 - 子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。只能是这两种情况之一
- 一个类中声明有 n 个构造器,最多有 n-1 个构造器中使用了
this(形参列表),剩下的那个一定使用super(形参列表)
常见错误
如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则会编译出错。
举例 1:
class A{
}
class B extends A{
}
class Test{
public static void main(String[] args){
B b = new B();
// A 类和 B 类都是默认有一个无参构造,B 类的默认无参构造中还会默认调用 A 类的默认无参构造
// 但是因为都是默认的,没有打印语句,看不出来
}
}举例 2:
class A{
A(){
System.out.println("A 类无参构造器");
}
}
class B extends A{
}
class Test{
public static void main(String[] args){
B b = new B();
// A 类显示声明一个无参构造,
// B 类默认有一个无参构造,
// B 类的默认无参构造中会默认调用 A 类的无参构造
// 可以看到会输出“A 类无参构造器"
}
}举例 3:
class A{
A(){
System.out.println("A 类无参构造器");
}
}
class B extends A{
B(){
System.out.println("B 类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
// A 类显示声明一个无参构造,
// B 类显示声明一个无参构造,
// B 类的无参构造中虽然没有写 super(),但是仍然会默认调用 A 类的无参构造
// 可以看到会输出“A 类无参构造器" 和 "B 类无参构造器")
}
}举例 4:
class A{
A(){
System.out.println("A 类无参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B 类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
// A 类显示声明一个无参构造,
// B 类显示声明一个无参构造,
// B 类的无参构造中明确写了 super(),表示调用 A 类的无参构造
// 可以看到会输出“A 类无参构造器" 和 "B 类无参构造器"
}
}举例 5:
class A{
A(int a){
System.out.println("A 类有参构造器");
}
}
class B extends A{
B(){
System.out.println("B 类无参构造器");
}
}
class Test05{
public static void main(String[] args){
B b = new B();
// A 类显示声明一个有参构造,没有写无参构造,那么 A 类就没有无参构造了
// B 类显示声明一个无参构造,
// B 类的无参构造没有写 super(...),表示默认调用 A 类的无参构造
// 编译报错,因为 A 类没有无参构造
}
}举例 6:
class A{
A(int a){
System.out.println("A 类有参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B 类无参构造器");
}
}
class Test06{
public static void main(String[] args){
B b = new B();
// A 类显示声明一个有参构造,没有写无参构造,那么 A 类就没有无参构造了
// B 类显示声明一个无参构造,
// B 类的无参构造明确写 super(),表示调用 A 类的无参构造
// 编译报错,因为 A 类没有无参构造
}
}举例 7:
class A{
A(int a){
System.out.println("A 类有参构造器");
}
}
class B extends A{
B(int a){
super(a);
System.out.println("B 类有参构造器");
}
}
class Test07{
public static void main(String[] args){
B b = new B(10);
// A 类显示声明一个有参构造,没有写无参构造,那么 A 类就没有无参构造了
// B 类显示声明一个有参构造,
// B 类的有参构造明确写 super(a),表示调用 A 类的有参构造
// 会打印“A 类有参构造器" 和 "B 类有参构造器"
}
}举例 8:
class A{
A(){
System.out.println("A 类无参构造器");
}
A(int a){
System.out.println("A 类有参构造器");
}
}
class B extends A{
B(){
super(); // 可以省略,调用父类的无参构造
System.out.println("B 类无参构造器");
}
B(int a){
super(a); // 调用父类有参构造
System.out.println("B 类有参构造器");
}
}
class Test8{
public static void main(String[] args){
B b1 = new B();
B b2 = new B(10);
}
}总结 this 与 super
this 和 super 的意义
this 表示当前对象。在构造器和非静态代码块中,表示正在 new 的对象。在实例方法中,表示调用当前方法的对象。
super 用来引用父类声明的成员。
this 和 super 的使用格式
this
this.成员变量:表示当前对象的某个成员变量,而不是局部变量this.成员方法:表示当前对象的某个成员方法,完全可以省略this.this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
super
super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中被声明super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中被声明super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错
子类对象实例化全过程
package javacode.oop2;
/**
* Description:
*/
public class ParentChildProcess {
}
class Creature {
public Creature() {
System.out.println("Creature 无参数的构造器");
}
}
class Animal extends Creature {
public Animal(String name) {
System.out.println("Animal 带一个参数的构造器,该动物的 name 为" + name);
}
public Animal(String name, int age) {
this(name);
System.out.println("Animal 带两个参数的构造器,其 age 为" + age);
}
}
class Dog extends Animal {
public Dog() {
super("汪汪队阿奇", 3);
System.out.println("Dog 无参数的构造器");
}
public static void main(String[] args) {
new Dog();
}
}面向对象特征之一:多态性
对象的多态性
多态性,是面向对象中最重要的概念。
说白了,就是多约束的对象类型可以赋值给少约束的对象类型,但是反过来不行。
就像 ts 有两个变量 a 和 b,a 是 HTMLElement 类型,b 是 HTMLCanvasElement 类型。
下面举个例子:
let a = {
a: 1,
b: 2,
c: 3
}
let b = {
a: 1,
}
// ok 多约束对象类型赋值给少约束类型
b = a;
// no
// a = b;多态的理解
Java 引用变量有两个类型:编译时类型和运行时类型。 编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。 简称:编译时,看左边;运行时,看右边。
- 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
- 多态情况下,“看左边”看的是父类的引用(父类中不具备子类特有的方法), “看右边”看的是子类的对象(实际运行的是子类重写父类的方法)
多态的使用前提:类有继承关系。
为什么需要多态性(polymorphism)?
开发中,有时我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时, 无法确定它具体的类型,只能确定它是某个系列的类型。
多态的好处和弊端
好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。
如何向上或向下转型
向上转型自动完成。向下转型语法:(子类类型)父类变量。
public class ClassCastTest {
public static void main(String[] args) {
// 没有类型转换
Dog dog = new Dog(); // dog 的编译时类型和运行时类型都是 Dog
// 向上转型
Pet pet = new Dog(); // pet 的编译时类型是 Pet,运行时类型是 Dog
pet.setNickname("小白");
pet.eat(); // 可以调用父类 Pet 有声明的方法 eat,但执行的是子类重写的 eat 方法体
// pet.watchHouse(); // 不能调用父类没有的方法 watchHouse
Dog d = (Dog) pet;
System.out.println("d.nickname = " + d.getNickname());
d.eat(); // 可以调用 eat 方法
d.watchHouse(); // 可以调用子类扩展的方法 watchHouse
Cat c = (Cat) pet; // 编译通过,因为从语法检查来说,pet 的编译时类型是 Pet,
// Cat 是 Pet 的子类,所以向下转型语法正确
// 这句代码运行报错 ClassCastException,因为 pet 变量的运行时类型是 Dog,Dog 和 Cat 之间是没有继承关系的
}
}instanceof 关键字
为了避免 ClassCastException 的发生,Java 提供了 instanceof 关键字,给引用变量做类型的校验。代码格式如下:
// 如果对象 a 是数据类型 A 的对象或数据类型 A 的子类的对象,那么返回 true,反之返回 false
对象 a instanceof 数据类型 A说明:
- 只要用 instanceof 判断返回 true 的,那么强转为该类型就一定是安全的,不会报 ClassCastException 异常
- 如果对象 a 属于类 A 的子类 B,a instanceof A 的值也为 true
Object 类的使用
理解根父类
类 java.lang.Object 是类层次结构的根类,即所有其它类的父类。每个类都使用 Object 作为超类。
Object 类型的变量与除 Object 以外的任意引用数据类型的对象都存在多态引用。
method(Object obj){} // 可以接收任何类作为其参数
Person o = new Person();
method(o);如果一个类没有特别指定父类,那么默认则继承自 Object 类。
public class Person {
...
}
// 等价于
public class Person extends Object {
...
}Object 类的方法
根据 JDK 源代码及 Object 类的 API 文档,Object 类当中包含的方法有 11 个。这里我们主要关注其中的 6 个。
equals()
先说说 == 的用法。
基本类型比较值,只要两个变量的值相等,即为 true。
int a=5;
if(a==6){…}引用类型比较引用(是否指向同一个对象),只有指向同一个对象时,== 才返回 true。
Person p1=new Person();
Person p2=new Person();
if (p1==p2){…}用 == 进行比较时,符号两边的数据类型必须兼容(可自动转换的基本数据类型除外),否则编译出错。
equals() 方法存在于 Object 类上,所有类都继承了 Object,也就获得了 equals() 方法。
如果只比较引用类型,Object 类源码中 equals() 的作用与 == 相同,比较是否指向同一个对象。
格式:
obj1.equals(obj2)特例
类 File、String、Date 及包装类(Wrapper Class)中重写了 Object 类的 equals() 方法。它们用 equals() 方法进行比较时, 比较的是类型及内容,而不考虑引用的是否是同一个对象。
当自定义使用 equals() 时,可以重写。用于比较两个对象的“内容”是否都相等。
重写 equals() 方法的原则:
- 对称性:如果 x.equals(y) 返回是
true,那么 y.equals(x) 也应该返回是true - 自反性:x.equals(x) 必须返回是
true - 传递性:如果 x.equals(y) 返回是 true,而且 y.equals(z) 返回是 true,那么 z.equals(x) 也应该返回是 true
- 一致性:如果 x.equals(y) 返回是 true,只要 x 和 y 内容一直不变,不管你重复 x.equals(y) 多少次,返回都是 true
- 任何情况下,x.equals(null) 永远返回 false
- x.equals(和 x 不同类型的对象) 永远返回 false
== 和 equals() 的区别
- == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址
- equals 属于 java.lang.Object 类里面的方法,如果该方法没有被重写过默认与 == 等效; 我们可以看到 String 等类的 equals 方法是被重写过的,但因为 String 类在日常开发中用的比较多, 形成了 equals 是比较值的错误观点
- 通常情况下,重写 equals 方法,会比较类中的相应属性是否都相等
toString()
方法签名:public String toString()。
默认情况下,toString() 返回的是 <对象的运行时类型>@<对象的 hashCode 值的十六进制形式>。
在 String 与其它类型数据进行连接操作时,其它类型数据自动调用 toString() 方法。
Date now=new Date();
System.out.println(“now=”+now); // 相当于
System.out.println(“now=”+now.toString());如果我们直接 System.out.println(对象),默认会自动调用这个对象的 toString() 方法。 Java 的引用数据类型的变量中存储的实际上时对象的内存地址,但是 Java 对程序员隐藏内存地址信息, 所以不能直接将内存地址显示出来,所以当你打印对象时,JVM 帮你调用了对象的 toString()。
例如自定义的 Person 类:
public class Person {
private String name;
private int age;
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}clone()
// Object 类的 clone() 的使用
public class CloneTest {
public static void main(String[] args) {
Animal a1 = new Animal("花花");
try {
Animal a2 = (Animal) a1.clone();
System.out.println("原始对象:" + a1);
a2.setName("毛毛");
System.out.println("clone 之后的对象:" + a2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Animal implements Cloneable{
private String name;
public Animal() {
super();
}
public Animal(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Animal [name=" + name + "]";
}
@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
}finalize()
- 当对象被回收时,系统自动调用该对象的 finalize() 方法。(不是垃圾回收器调用的,是本类对象调用的) 永远不要主动调用某个对象的 finalize 方法,应该交给垃圾回收机制调用
- 什么时候被回收?当某个对象没有任何引用时,JVM 就认为这个对象是垃圾对象, 就会在之后不确定的时间使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize() 方法
- 子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连接资源。 如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象
- 在 JDK 9 中此方法已经被标记为过时的
public class FinalizeTest {
public static void main(String[] args) {
Person p = new Person("Peter", 12);
System.out.println(p);
p = null; // 此时对象实体就是垃圾对象,等待被回收。但时间不确定。
System.gc(); // 强制性释放空间
}
}
class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// 子类重写此方法,可在释放对象前进行某些操作
@Override
protected void finalize() throws Throwable {
System.out.println("对象被释放--->" + this);
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}getClass()
public final Class<?> getClass():获取对象的运行时类型。
因为 Java 有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致, 因此如果需要查看这个变量实际指向的对象的类型,需要用 getClass() 方法。
public static void main(String[] args) {
Object obj = new Person();
System.out.println(obj.getClass()); // 运行时类型
}结果:
class com.atguigu.java.PersonhashCode()
public int hashCode():返回对象的 hash 值。
public static void main(String[] args) {
System.out.println("AA".hashCode()); // 2080
System.out.println("BB".hashCode()); // 2112
}native 关键字的理解
使用 native 关键字说明这个方法是原生函数,也就是这个方法是用 C/C++ 等非 Java 语言实现的,并且被编译成了 DLL, 由 Java 去调用。
本地方法是有方法体的,用 c 语言编写。由于本地方法的方法体源码没有对我们开源,所以我们看不到方法体。
在 Java 中定义一个 native 方法时,并不提供实现体。
- 为什么要用 native 方法?
Java 使用起来非常方便,然而有些层次的任务用 java 实现起来不容易,或者我们对程序的效率很在意时。 例如:Java 需要与一些底层操作系统或某些硬件交换信息时的情况。native 方法正是这样一种交流机制, 它为我们提供了一个非常简洁的接口,而且我们无需去了解 Java 应用之外的繁琐的细节。
- native 声明的方法,对于调用者,可以当做和其他 Java 方法一样使用
native method 的存在并不会对其他类调用这些本地方法产生任何影响, 实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM 将控制调用本地方法的所有细节。