1. 继承性

1.1 继承性的理解

将具有共性的一类对象的共同特征抽离出来,形成一个父类,这个父类具有这些对象的基本属性。如Student类、Custoer类、Teacher类以及Waiter类等都具有姓名、年龄等人所具有的属性和方法,故使用Person类作为这些类的父类,只要这些类继承了Person类,则它们就拥有了相关的属性和方法。

image-20210225144256530.png

继承性的好处:

  1. 减少了代码冗余,提高代码复用性
  2. 便于功能的扩展
  3. 为多态性的使用提供了前提

1.2 继承性的使用

格式:class A extends B{}

表示A继承BA被称为“派生类”、“子类”、“subclass”,B被称为“超类”、“基类”、“父类”、“superclass”。

  • 子类继承父类以后,就自动获得父类中声明的所有属性和方法。特别的,父类中声明为private的属性或方法,子类继承父类后,仍然认为获取了父类中的私有结构,只是因为封装性的影响,使得子类不能直接调用这些结构而已。
  • 继承父类后,子类仍然可以声明自己的特有属性和方法,以实现功能扩展注意构造函数中必须调用父类构造函数
/*
Person类
*/
public class Person {
    String name;
    int age;

    public Person() {
    
    }

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

    public void eat() {
    
    }

    public void sleep() {
    
    }
}
/*
Student类
*/
public class Student extends Person {
    String major;

    public Student(String name, int age, String major) {
        super();
        this.age = age;
        this.name = name;
        this.major = major;
    }

}

1.3 继承性的说明

Java中关于继承性的规定:

  • 一个类可以被多个子类继承。
  • 一个类只能有一个父类(单继承性)。
  • 子类父类是相对的概念,子类仍然可以有子类。如C继承于B,B继承于A,则A是C的间接父类,B是C的直接父类。
  • 子类继承父类以后就获取了直接父类以及所有间接父类中声明的属性和方法。
  • 一个类如果没有显式声明一个父类的话,则默认继承自java.lang.Object

2. 方法的重写

2.1 方法重写的理解

  1. 重写:子类继承父类后,可以对父类中同名同参数的方法进行覆盖操作
  2. 应用:重写以后,当创建子类对象后,通过子类对象调用父类中被重写的方法,实际执行的是子类中的方法。
public class Person {
    String name;
    int age;

    public Person() {

    }

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

    public void eat() {
        System.out.println("吃饭");
    }

    public void walk(int distance) {
        System.out.println("走路, 走了" + distance + "公里");
    }
}
public class Student extends Person {
    String major;

    public Student(String major) {
        this.major = major;
    }

    public Student() {

    }

    // 重写父类方法
    public void eat() {
        System.out.println("学生吃饭");
    }

    public void study() {
        System.out.println("学习,专业是:" + major);
    }
}

运行结果:

学生吃饭
走路, 走了0公里
学习,专业是:计算机科学与技术

被重写的方法eat()实际使用的是子类中的方法。

2.2 方法重写的注意点

关于重写有如下约定:子类中的方法叫“重写的方法”,父类中的叫“被重写的方法”。

有如下注意点:

  1. 子类重写的方法名、形参列表与父类被重写的方法名、形参列表相同
  2. 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符

    • 特殊情况:子类不能声明父类中private的方法。如果写了,则并不认为是方法重写。
  3. 返回值类型

    • 如果父类中被重写的方法返回值是void,则子类重写的方法的返回值必须是void
    • 设父类被重写的方法的返回值是A类型,则子类重写的方法的返回值类型可以试A类本身或A类的子类。
    • 父类被重写的方法返回值是基本数据类型,则子类重写的方法的返回值必须是相同基本数据类型。
  4. 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型。

通常情况下,重写的方法和被重写的方法返回值类型是相同的。实际开发中直接复制父类中被重写的方法到子类即可。

子类和父类中同名同参数的方法,要么都声明为static的,要么都声明为非static的。当二者都声明为static时,不能称之为重写。

3. Super 关键字

当父类中的方法被子类重写,但是由于某些原因需要重新使用父类中被重写的方法时,可以使用super.方法名()的方式调用父类的方法
  1. super关键字可以理解为:父类的
  2. super关键字可以用来调用:属性、方法、构造器

3.1 super调用属性和方法

  • 我们可以在子类的方法或构造器中使用super.属性super.方法的方式,显式调用父类中声明的属性或方法。但是,通常情况下,我们习惯省略super.
  • 特殊情况:当子类和父类中定义了同名的属性时,我们想要在子类中调用父类中声明的属性,则必须显式地使用super.属性方式表明调用的是父类中的属性。
  • 特殊情况:子类重写了父类中的方法之后,我们希望在子类中调用父类中的方法时,必须使用super.方法的方式,表明调用的是父类中的方法。
public class Student extends Person {
    String major;

    public Student(String major) {
        this.major = major;
    }

    public Student() {

    }

    // 重写父类方法
    public void eat() {
        super.eat();// 调用父类的方法
        System.out.println("学生吃饭");
    }

    public void study() {
        String name = super.name;// 调用父类属性
        System.out.println("学习,专业是:" + major);
    }
}

3.2 super调用构造器

  • 我们可以在子类构造器中显式使用super(形参列表)方式调用父类中声明的指定构造器
  • super(形参列表)使用时,必须声明在子类构造器的首行。
  • 我们在构造器中,针对于this(形参列表)super(形参列表)只能二选一,不可同时出现。
  • 如果子类构造器没有使用super(形参列表)形式,则默认会调用父类的空参构造函数,即super()
  • 在类的多个构造器中,至少有一个类的构造器中使用了super(形参列表),调用了父类构造器。

4. 子类对象实例化过程

  • 从结果上看:子类继承父类以后就获取了父类声明的属性或方法。创建子类的对象,在堆空间中加载会加载所有父类中声明的属性。
  • 从过程上来看:

    当我们通过子类的构造器创建子类对象时,我们一定会直接或间接调用其父类的构造器,进而调用父类的父类的构造器,直到调用了java.lang.Object类中空参构造器为止。正因为加载过所有父类结构,所以才可以看到内存中有父类中的结构,子类对象才可以考虑进行调用。
    image-20210225190417520.png

虽然创建子类对象时调用了父类的构造器,但是自始至终只创建了一个对象,即为new 子对象

5. 多态性

5.1 理解多态性

多态性可以理解为一个事物的多种形态,例如一只哈士奇,可以看做“哈士奇”类,也可以看做“狗”类,也可以看做“动物”类。

对象的多态性:父类的引用指向子类的对象,或者说子类对象赋给父类变量,叫做对象的多态性:

Person p = new Man();// 多态性的体现

5.2 多态的使用

有了对象多态性以后,在编译期只能调用父类中声明的方法,但在执行期实际执行的是子类重写的方法。(虚拟方法调用)

编译看左边,运行看右边

5.3 多态的使用前提

  • 有类的继承关系
  • 有方法的重写

5.4 为什么要使用多态性

public class AnimalTest {
    public static void main(String[] args) {
        AnimalTest test = new AnimalTest();
        test.func(new Dog());
        test.func(new Cat());
    }

    public void func(Animal animal) {
        animal.eat();
        animal.shout();
    }
}

class Animal {
    public void eat() {
        System.out.println("动物:饮食");
    }

    public void shout() {
        System.out.println("动物:叫");
    }
}

class Dog extends Animal {
    public void eat() {
        System.out.println("狗吃骨头");
    }

    public void shout() {
        System.out.println("汪汪汪");
    }
}

class Cat extends Animal {
    public void eat() {
        System.out.println("猫吃鱼");
    }

    public void shout() {
        System.out.println("喵喵喵");
    }
}

运行结果

狗吃骨头
汪汪汪
猫吃鱼
喵喵喵

方法的形参使用了父类,而实际调用时传入了子类对象,从而实现一个方法可以传入父类的所有子类,并执行子类中重写的相关方法。

5.5 多态性的注意点

对象的多态性,只适用于方法,不适用于属性(编译和运行都看左边)

5.6 虚拟方法调用

  • 正常方法的调用:
Person p = new Person();
p.getInfo();
Student s = new Student();
s.getinfo();
  • 虚拟方法调用(多态情况下):子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
Person p = new Student();
p.getInfo();
  • 编译时类型和运行时类型:编译时pPerson类型,而方法调用是在运行时确定的,所以调用的是Student子类中的方法。

方法的重载与重写:

重载,是指允许存在多个同名方法,而这些方法的参数是不同的。编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。

所以,对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;

而对于多态,只有等到方法调用的那一刻,编译器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。

Bruce Eckel:“不要犯傻,如果它不是晚绑定,它就不是多态”

5.7 向下转型

x instanceof A:检验x是否是A的对象,返回值为boolean类型。

  • 要求x所属的类与A类必须是子类和父类关系,否则编译错误
  • 如果x属于A类的子类Bx instanceof A的返回值仍为true

假设存在如下两个类:

public class Person {
    String name;
    int age;

    public Person() {

    }

    public void eat() {
        System.out.println("人吃饭");
    }
}
public class Student extends Person {
    String major;

    public Student() {
        super();
    }

    public void eat() {
        System.out.println("学生吃饭");
    }
}

两个类是继承关系,当使用了Person p = new Student();时,体现了多态性。但是无法通过p调用子类中特有的属性和方法,这是因为在编译期pPerson类型,直接调用会报错。如何解决这一问题呢?Java引入了向下转型机制:

Student s = (Student)p;

即使用强制转换符将变量强制转换为子类对象。之所以可以这么用是因为在内存中实际存在的是子类对象,只是被声明为父类的对象。

多态性其实就是向上转型,这和基本数据类型中的强制类型转换和自动类型提升类似

向下转型属于强制转换,与基本数据类型的强转一样存在风险,如果转换的两个对象不是继承关系,则会出现ClassCastException异常。

5.8 instanceof操作符

为了解决向下转型不安全的问题,引入了instanceof关键字,即在强制转换之前可以判断两者是不是同一类对象或者为继承关系。

if(p instanceof Student){
    Student s = (Student)p;
}
在向下转型前一般使用该关键字进行判断。

向下转型常见问题:

  • 编译通过,运行不通过

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

    此时由于左边类型和右边强制转换的类型相同,编译器无法检查出错误,但是Teacher类与Student类不可互相转换。

  • 编译通过,运行也通过

    Object o = new Student();
    Person p = (Person)o;

    即将对象声明为其间接父类,再将间接父类向下转型为直接父类,此时编译与运行不会出错,是合法的。

  • 编译不通过

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

    子类不可以声明为其它不同的类型。

6. Object类的使用

6.1 Object类的特点

Object是所有Java类的根父类,如果类的声明中没有显式使用extends关键字指明父类的话,默认继承自Object,因此,Object类中的功能(属性、方法)就具有通用性。
class Person {
  
}

等价于:

class Person extends Object{
  
}
Object类中只声明了一个空参的构造器

6.2 Object类中的部分成员

  • 属性:无
  • 方法(部分):

    • equals():比较两个对象
    • toString():将对象信息转换为字符串
    • getClass():获取类结构
    • hashCode():获取哈希值
    • clone():复制对象
    • finalize():垃圾回收机制在回收对象之前调用该方法
    • wait()notify()notifyAll():多线程中用于通信

6.3 equals()方法

==equals()的区别:

  • ==:运算符,可以使用在基本数据类型变量和引用数据类型变量中,如果比较的是基本数据类型变量,则比较两个变量保存的数据是否相同(类型可以不同),如果比较的是两个引用数据类型,则比较两个对象的地址值是否相等。
  • equals()方法:不能用于基本数据类型,只适用于引用数据类型。使用方法:a.equals(b),表示判断a对象和b对象是否相等。
  • 值得注意的是,在Object类中定义的equals()方法其实使用了==作为判断条件:
// Java源码Object类中定义的equals()方法
public boolean equals(Object obj) {
    return (this == obj);
}
因此,如果没有重写Object中的equals()方法,那么在比较两个对象时与==相同。在Java中,StringDateFile等类内部重写了equals()方法,不再比较两个对象的地址值,而是比较对象内部关键属性的值。

通常情况下,如果自定义类使用了equals()方法,通常情况下不希望对地址值进行比较,而是比较对应的对象属性值,此时可以重写equals()方法。原则:比较两个对象的实体内容是否相同。

//equals方法重写举例(简单但是不严谨)
@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }

    if (obj instanceof Customer) {
        Customer cust = (Customer) obj;// 向下转型
        // 比较两个对象的属性
        if (this.age == cust.age && this.name.equals(cust.name)) {
            return true;
        }
    }

    return false;
}
// 更健壮的写法
@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Customer other = (Customer) obj;// 向下转型
    if (age != other.age)
        return false;
    if (name == null) {
        if (other.name != null)
            return false;
    } else if (!name.equals(other.name))
        return false;
    return true;
}

重写equals()方法的原则:

  • 对称性:如果x.equals(y)的返回值是true,那么y.equals(x)的返回值也应是true
  • 自反性:x.equals(x)的值必须是true
  • 传递性:如果x.equals(y)的返回值是truey.equals(z)的返回值是true,那么x.equals(z)的返回值也应是true
  • 一致性:如果x.equals(y)的返回值是true,只要xy内容一直不变,不管重复比较多少次返回值都应该是true
  • 任何情况下,x.equals(null),永远返回falsex.equals(非x同类的对象)永远返回false

6.4 toString()方法的使用

  • 当输出一个对象的引用时,实际输出的是当前对象的toString()
  • Object类中toString()的定义:
// Object对象默认toString方法
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
如果一个类没有重写toString()方法,则返回的是这个类的类型和哈希值。而StringDateFile等类内部重写了toString()方法,所以在输出时可以呈现对应的更加直观的值。在实际开发中,我们也希望能够打印关于类内容的信息,因此可以重写toString()方法。

重写示例:

@Override
public String toString() {
    return "Customer [name=" + name + ", age=" + age + "]";
}

7. 单元测试

  1. 导入JUnitjar包
  2. 创建Java类进行单元测试,此时的Java类要求:

    1. 此类是公共的
    2. 要提供一个公共的无参构造器
  3. 在此类中声明单元测试方法,要求:

    1. 方法权限是public
    2. 没有返回值
    3. 没有形参
  4. 在单元测试方法上声明@Test注解并在单元测试类中导入org.junit.Test
  5. 声明好单元测试方法后,就可以在方法体内测试相关代码
  6. 写完代码以后,左键双击单元测试方法名,右键>run as>JUnit Test即可进行测试(Eclipse)
如果执行结果没有任何异常,则显示绿条,如果执行结果出现异常,则显示红条。

8. 包装类(Wrapper)的使用

8.1 基本概念

  • 针对八种基本数据类型定义的相应引用类型——包装类(封装类)
  • 有了类的特点,就可以调用类中的方法,Java才是真正的面向对象
基本数据类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
booleanBoolean
charCharacter

8.2 基本数据类型与包装类的转换

基本数据类型 ---->包装类,调用包装类的构造器

// 基本数据类型 to 包装类
@Test
public void test1() {
    int num1 = 10;
    Integer in1 = new Integer(num1); // 引入自动装箱以后此方法不再推荐使用
    System.out.println(in1.toString());

    Integer in2 = new Integer("123");
    System.out.println(in2.toString());

    Float f1 = new Float(1.3);
    Float f2 = new Float("1.3");

    Boolean b1 = new Boolean(true);
    Boolean b2 = new Boolean("TrUe");
}

包装类 ---> 基本数据类型,调用包装类的xxxValue()

// 基本数据类型 to 包装类
@Test
public void test1() {
    int num1 = 10;
    Integer in1 = new Integer(num1);
    System.out.println(in1.toString());

    Integer in2 = new Integer("123");
    System.out.println(in2.toString());

    Float f1 = new Float(1.3);
    Float f2 = new Float("1.3");

    Boolean b1 = new Boolean(true);
    Boolean b2 = new Boolean("TrUe");
}

8.3 自动装箱与自动拆箱(jdk5.0新特性)

@Test
public void test3() {
    // 自动装箱
    int num1 = 10;
    Integer in1 = num1;
    boolean b1 = true;
    Boolean b01 = b1;

    // 自动拆箱
    int num2 = in1;
    boolean b2 = b01;
}

8.4 包装类与String类型的转换

包装类 --> String

  • 连接运算
  • 调用String.valueOf(),不管是基本数据类型还是包装类都可以
@Test
public void test4(){
    // 转换方式一:连接运算
    int num1 = 10;
    String s1 = num1 + "";

    // 装换方式二:调用valueOf()方法
    float f1 = 12.3f;
    String s2 = String.valueOf(f1);
}

String --> 包装类:调用包装类的parseXxx()方法

@Test
public void test5() {
    String s1 = "10";
    int i1 = Integer.parseInt(s1);
    float f1 = Float.parseFloat(s1);
    String s2 = "true";
    boolean b1 = Boolean.parseBoolean(s2);
}
可能会报NumberFormatException
最后修改:2021 年 03 月 08 日
如果觉得我的文章对你有用,请随意赞赏