Java笔记6封装继承多态

55 阅读16分钟

IDEA

image.png
image.png

本质就是文件夹 方便区分同名类 控制访问范围

语法

package 包名

命名

只能包换数字 字母 下划线 小圆点 但不能数字开头 不能是关键字
一般是公司名.项目名.业务模块名

常用的包

  • java.lang.* 基本包 默认引入 不需要再引入
  • java.util.* 系统提供的工具包 工具类 使用Scanner
  • java.net.* 网络包 网络开发
  • java.awt.* java的页面开发 GUI

引入

import 包
引入包的目的主要是使用该包下面的类
import java.util.Scanner;就是引入一个类Scanner
import java.util.*;就是把java.util包下所有类都引入

注意事项

package的作用是声明当前类所在包 需要放在类/文件最上面
import指令放在package下面 类定义前面 可以有多句 顺序无要求

访问修饰符

用于控制方法和属性的访问权限
修饰符可以用来修饰类中的属性 成员方法 类
只有默认和public可以修饰类 且村寻访问权限的特点

  • public:公开级别 对外公开
  • protected:受保护级别 对子类和同一个包的类公开
  • 无修饰符:默认级别 向同一个包的类公开
  • private:私有级别 只有类本身可以访问 不对外公开
访问级别修饰符同类同包子类不同包
公开public
受保护protected×
默认××
私有private×××

封装/encapsulation

把抽象出的数据(属性)和对数据的操作(方法)封装在一起 数据被保护在内部 程序的其他部分只有通过被授权的操作(方法)才能对数据进行操作 实现隐藏细节

实现

  1. 将属性进行私有化private(不能直接修改属性)
  2. 提供一个公共的(public)set方法 用于对属性判断并赋值
    public void setXxx(类型 参数名){//Xxx表示某个属性
        //加入数据验证的业务逻辑  
        属性=参数名;
    }  
    
  3. 提供一个公共的(public)get方法 用于获取属性的值
    public 数据类型 getXxx(){//权限判断 Xxx某个属性
        return xx;
    }
    

写一个小程序 不能随便查看人的年龄 并对设置的年龄进行验证 在-120之间 名字在2-6个字符之间

public class Test {
    public static void main(String[] args) {
        Person p=new Person();
        p.setName("tom");
        p.setAge(18);
        System.out.println(p.info());//信息为 name=tom age=18
    }
}
class Person{
    public String name;//公共
    private int age;//私有

    public void setName(String name) {
        if(name.length()>=2&&name.length()<=6){//验证名字
            this.name = name;
        }else{
            System.out.println("名字长度要2-6个字符");
        }
    }
    public void setAge(int age) {
        if(age>=1&&age<=120){//验证年龄
            this.age = age;
        }else{
         System.out.println("年龄要在1-120之间");
        }
    }

    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }

    public String info(){
        return "信息为 name="+name+" age="+age;
    }
}

构造器与setXxx结合

如果直接使用构造器 则绕过set方法 验证失效 所以可以把set放在构造器中 这样仍能验证

public class Test {
    public static void main(String[] args) {
        Person p=new Person("tom",18);
        System.out.println(p.info());//信息为 name=tom age=18
    }
}
class Person{
    public String name;//公共
    private int age;//私有

    public void setName(String name) {
        if(name.length()>=2&&name.length()<=6){
            this.name = name;
        }else{
            System.out.println("名字长度要2-6个字符");
        }
    }
    public void setAge(int age) {
        if(age>=1&&age<=120){
            this.age = age;
        }else{
         System.out.println("年龄要在1-120之间");
        }
    }

    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }

    public String info(){
        return "信息为 name="+name+" age="+age;
    }

    public Person(String name,int age){
//        this.name=name; 这样就使set的验证失效了
//        this.age=age;
        setName(name);
        setAge(age);
    }
}

示例

创建程序 在其中定义两个类:Account和AccountTest类 体会Java的封装性
Account类要求具有属性:姓名(长度为2位3位或4位) 余额(必须>20) 密码(必须是六位)
如果不满足 则给出提示信息 并给默认值(程序员自己定)
通过setXxx的方法给Account的属性赋值
在AccountTest中测试

public class AccountTest {
    public static void main(String[] args) {
        Account account=new Account();
        account.setName("jack");
        account.setBalance(60);
        account.setPsw("123456");
        account.showInfo();
        //账号信息 名字=jack 余额=60.0 密码=123456
    }
}

class Account{
    private String name;
    private double balance;
    private String psw;

    //两个构造器
    public Account(String name,double balance,String psw){
        this.setName(name);
        this.setBalance(balance);
        this.setPsw(psw);
    }
    public Account(){

    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        if(name.length()>=2&&name.length()<=4){
            this.name = name;
        }else{
            System.out.println("名字长度应为2-4位 默认名字为张伟");
            this.name="张伟";
        }
    }

    public double getBalance() {
        return balance;
    }
    public void setBalance(double balance) {
        if (balance >= 20) {
            this.balance = balance;
        }else{
            System.out.println("余额必须大于20 默认100");
            this.balance=100;
        }
    }

    public String getPsw() {
        return psw;
    }
    public void setPsw(String psw) {
        if(psw.length()==6) {
            this.psw = psw;
        }else{
            System.out.println("密码长度必须为6位,默认000000");
            this.psw="000000";
        }
    }

    public void showInfo(){//显示账号信息
        System.out.println("账号信息 名字="+name+" 余额="+balance+" 密码="+psw);
    }
}

继承/extends

两个类的属性和方法有很多是相同的 为了提高代码复用性拓展性维护性 可以使用继承

定义

当多个类中存在相同的属性和方法时 可以从这些类中抽象出父类
在父类中定义相同的属性和方法
子类会自动用于父类定义的属性和方法 在子类中无需重复定义
只需要通过extends来声明继承父类
class 子类 extends 父类{}

image.png

细节

  1. 子类继承了父类所有的属性和方法 非私有的属性和方法可以在子类直接访问 但私有属性和方法需要通过父类提供的公共方法访问
  2. 子类必须调用父类的构造器 完成父类的初始化
  3. 创建子类时 不论使用什么构造器 默认会调用父类的无参构造器 因为子类构造器中会默认带有super()语句 若父类没有无参构造器 则必须在子类的构造器中使用super去指定父类的哪个构造器完成对父类的初始化工作 否则编译无法通过
  4. super()函数在子类构造器中调用父类的构造器时使用 而且必须要在构造器的第一行
  5. this()函数在同一类中从某个构造器调用另一个重载版构造器 而且必须要在构造器的第一行
  6. 若希望指定调用父类的某个构造器 则在子类构造器中显式地调用一下:super(参数列表)
  7. super()和this()都只能放在构造器第一行 因此两个方法不能共存在一个构造器
  8. java所有类都是Object类的子类 Object是所有类的基类
  9. 父类构造器的调用不限于直接父类 将一直往上追溯到Object类(顶级父类)
  10. java是单继承机制 最多只能直接继承一个父类
  11. 不能滥用继承 子类父类必须满足is a的逻辑关系
public class Test {
    public static void main(String[] args) {
        Son son1=new Son();
        //调用了父类的无参构造器 //默认super()调用父类无参构造器
        //调用了子类1的无参构造器

        Son son2=new Son("tom",18);
        //调用了父类的无参构造器
        //调用了子类1的有参构造器
        //子类1初始化结果为 name=tom age=18

        Sons son3=new Sons();
        //调用了父类的有参构造器 //this(参数列表)调用了同类的指定构造器
        //父类初始化结果为 name=tom age=18
        //调用了子类2的有参构造器
        //子类2初始化结果为 name=tom age=18
        //调用了子类2的无参构造器

        Sons son4=new Sons("tom",18);
        //调用了父类的有参构造器 //写出super(参数列表)指定调用父类的哪个构造器
        //父类初始化结果为 name=tom age=18
        //调用了子类2的有参构造器
        //子类2初始化结果为 name=tom age=18
    }
}

class Father{//父类
    public String name;
    public int age;
    public Father(String name,int age){//父类有参构造器
        this.name=name;
        this.age=age;
        System.out.println("调用了父类的有参构造器");
        System.out.println("父类初始化结果为 name="+name+" age="+age);
    }
    public Father(){//父类无参构造器
        System.out.println("调用了父类的无参构造器");
    };
}
class Son extends Father{//子类1
    public Son(String name,int age){
        this.name=name;
        this.age=age;
        System.out.println("调用了子类1的有参构造器");
        System.out.println("子类1初始化结果为 name="+name+" age="+age);
    }
    public Son(){
        System.out.println("调用了子类1的无参构造器");
    }
}
class Sons extends Father {//子类2
    public Sons(){
        this("tom",18);//调用了Sons类的有参构造器
        System.out.println("调用了子类2的无参构造器");
    }
    public Sons(String name, int age) {
        super(name, age);//在子类的构造器中使用super去指定父类的哪个构造器完成对父类的初始化工作
        //super()和this()不能共存 这里不能有this()语句
        System.out.println("调用了子类2的有参构造器");
        System.out.println("子类2初始化结果为 name="+name+" age="+age);
    }

}

内存布局

  • 首先查看子类有没有该属性
    • 若子类有该属性 且可以访问 则返回信息
    • 若子类没有该属性 就查看父类有没有该属性
      • 若父类有该属性 且可以访问 就返回信息
      • 若父类没有该属性 就继续擦或者上级父类 直到Object
  • 方法同理
public class Test {
    public static void main(String[] args) {
        Son son=new Son();
        System.out.println(son.name);//大头儿子
        System.out.println(son.getAge());//39 通过父类的公共方法访问
        //System.out.println(son.age); 无法访问父类的private属性 会报错cannot access
        System.out.println(son.hobby);//旅游
    }
}

class GrandPa{//爷类
    String name="大头爷爷";
    String hobby="旅游";
}
class Father extends GrandPa{//父类
    String name="大头爸爸";
    private int age=39;
    public int getAge(){
        return age;
    }
}
class Son extends Father{//子类
    String name="大头儿子";
}

image.png

super关键字

代表父类的引用 用于访问父类的属性 方法 构造器

使用

  • 访问父类的非private属性:super.属性名
  • 访问父类的非private方法:super.方法名(参数列表)
  • 访问父类的构造器:super(参数列表) 只能放在构造器第一句
  • 用super访问不仅限于父类 若爷爷类或本类中有同名成员 也可以用super访问爷爷类的成员 若多个上级类中有同名的成员 遵循就近原则 查找规则是直接查找父类 有则返回 无则向上查找
  • 子类和父类中的成员重名时 为了访问父类的成员 必须通过super 若无重名 使用super this 直接访问是一样的效果

super和this

区别thissuper
访问属性访问本类中的属性 如果本类中没有此属性 则从父类中继续查找从父类开始查找属性
调用方法访问本类中的方法 如果本类中没有此方法 则称父类中继续查找从父类开始查找方法
调用构造器调用本类构造器 必须放在构造器首行调用父类构造器 必须放在子类构造器首行
特殊表示当前对象子类中访问父类对象

方法重写/覆盖/override

子类有一个方法和父类某个方法的名称 返回类型 形参列表一样 就说子类的这个方法覆盖了父类的方法

  • 子类的形参列表和方法名称要和父类完全一样
  • 子类的方法返回类型要和父类一样或者是父类返回类型的子类 比如父类返回Object 子类返回String
  • 子类方法不能缩小父类方法的访问权限 public>protected>默认>private

重载和重写

名称发生范围方法名形参列表返回类型修饰符
重载/overload本类必须一样类型 个数 顺序至少有一个不同无要求无要求
重写/override父子类必须一样相同子类和父类一致 或者子类是父类子类子类方法不能缩小父类方法的访问范围

示例

编写一个Person类 包括private属性name age 构造器 方法say(返回自我介绍的字符串) 编写一个Student类 继承Person类 增加private属性id score 以及构造器 定义say方法(返回自我介绍的信息)
在main中 分别创建Person和Student对象 调用say方法输出自我介绍

public class Test {
    public static void main(String[] args) {
        Person jack=new Person("jack",10);
        System.out.println(jack.say());
        //名字是jack 年龄是10

        Student smith=new Student("smith",20,123456,99.8);
        System.out.println(smith.say());
        //名字是smith 年龄是20 id=123456 成绩是99.8
    }
}
class Person{
    private String name;
    private int age;
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public Person(){
    }
    public String say(){
        return "名字是"+name+" 年龄是"+age;
    }
}
class Student extends Person{
    private int id;
    private double score;
    public Student(String name,int age,int id,double score){
        super(name,age);//调用父类构造器
        this.id=id;
        this.score=score;
    }
    public String say(){//方法重写
        return super.say()+" id="+id+" 成绩是"+score;//代码复用
    }
}

多态

方法或对象有多种形态 是面向对象的第三大特征 多态建立在封装和继承的基础之上

方法的多态

重写和重载体现多态 同名的方法可以传入不同的形参

对象的多态

  • 一个对象的编译类型和运行类型可以不一致
  • 编译类型在定义对象时就确定了 不能改变
  • 运行类型可以变化
  • 定义时编译类型 对象名=new 运行类型()
  • 编译类型和运行类型有同名方法时 执行运行类型的方法
//Cat和Dog是Animal的子类
public class Test {
    public static void main(String[] args) {
        Animal animal=new Cat();//animal的编译类型是Animal 运行类型是Cat
        animal=new Dog();//animal的运行类型改成了Dog 编译类型仍是Animal
    }
}

主人给动物喂食
动物有两类 猫狗 食物有两类 鱼骨头

public class Test {
    public static void main(String[] args) {
    Person tom=new Person("汤姆");
    Animal cat=new Cat("猫");
    Food fish=new Fish("鱼");
    tom.feed(cat,fish);
    //主人汤姆给猫吃鱼

    Person jack=new Person("杰克");
    Animal dog=new Dog("狗");
    Food bone=new Bone("骨头");
    jack.feed(dog,bone);
    //主人杰克给狗吃骨头
    }
}

class Person{
    private String name;
    public Person(String name){
        this.name=name;
    }
    //使用多态 可以统一管理主人喂食的问题
    //animal的编译类型是Animal 可以接收(指向)Animal子类的对象
    //food的编译类型是Food 可以接收(指向)Food子类的对象
    public void feed(Animal animal,Food food){
        System.out.println("主人"+name+"给"+animal.getAnimal()+"吃"+food.getFood());
    }
}

class Animal{
    private String animal;

    public String getAnimal() {
        return animal;
    }

    public void setAnimal(String animal) {
        this.animal = animal;
    }

    public Animal(String animal){
        this.animal=animal;
    }
}
class Cat extends Animal{
    public Cat(String cat){
        super(cat);
    }
}
class Dog extends Animal{
    public Dog(String dog){
        super(dog);
    }
}

class Food{
    private String food;

    public String getFood() {
        return food;
    }

    public void setFood(String food) {
        this.food = food;
    }

    public Food(String food){
        this.food=food;
    }
}
class Fish extends Food{
    public Fish(String fish){
        super(fish);
    }
}
class Bone extends Food{
    public Bone(String bone){
        super(bone);
    }
}

细节

  • 前提:两个对象(类)存在继承关系
  • 多态向上转型
    • 本质:父类的引用类型指向了子类的对象
    • 语法:父类类型 引用名=new 子类类型();
    • 特点:
      • 编译类型看左边 运行类型看右边
      • 可以调用父类的所有成员(遵循访问权限)
      • 调用成员类型由编译类型(父类)决定 不能调用子类的特有成员
      • 调用方法时从运行类型(子类)开始查找方法 最终运行效果看子类的具体实现
  • 多态向下转型
    • 语法:子类类型 引用名=(子类类型)父类引用;
    • 特点:
      • 只能强转父类的引用 不能强转父类的对象
      • 要求父类的引用必须指向的是当前目标类型的对象
      • 当向下转型后 可以调用子类类型中所有的成员
  • 属性不能重写 属性的值看编译类型
  • instanceOf比较操作符 用于判断对象的运行类型是否为xx类型或xx类型的子类型
public class Test {
    public static void main(String[] args) {
        //向上转型
        Animal animal=new Cat();
        Object obj=new Cat();//Object也是Cat的父类

        //animal.catchMouse();错误 不能调用子类特有成员
        animal.eat();//猫吃鱼 运行效果看子类的具体实现
        animal.run();//跑

        //向下转型
        Cat cat=(Cat)animal;
        cat.catchMouse();//猫抓老鼠

        System.out.println(animal.age);//10 属性没有重写之说 值取决于编译类型
        Cat cat1=new Cat();
        System.out.println(cat1.age);//20

        String str="hello";
        Object obj1=new Object();
        System.out.println(cat instanceof Animal);//true cat运行类型是Cat 是Animal的子类
        System.out.println(cat instanceof Cat);//true cat运行类型是Cat 是Cat类
        System.out.println(obj1 instanceof Cat);//false obj1运行类型是Object 不是Cat的子类
        System.out.println(str instanceof Object);//true str运行类型是String 是Object的子类
    }
}
class Animal{
    String name ="动物";
    int age=10;

    public void run(){
        System.out.println("跑");
    }
    public void eat(){
        System.out.println("吃");
    }
}
class Cat extends Animal{
    int age=20;
    public void eat(){//方法重写
        System.out.println("猫吃鱼");
    }
    public void catchMouse(){//特有方法
        System.out.println("猫抓老鼠");
    }
}

Java的动态绑定机制

  • 当调用对象方法的时候 该方法会和该对象的内存地址/运行类型绑定
  • 当调用对象属性时 没有动态绑定机制 哪里声明哪里使用 即调用哪个类的方法 就按哪个类的属性
public class Test {
    public static void main(String[] args) {
        //向上转型 a的编译类型是A 运行类型是AA
        A a=new AA();
        //调用方法时和运行类型绑定 如果运行类型没有就去找父类
        System.out.println(a.getI());//100
        System.out.println(a.sum1());//200
        System.out.println(a.sum2());//200

        //如果把AA的getI注释掉 则调用父类的getI i是父类的i 10
        System.out.println(a.getI());//10
        //如果把AA的sum1注释掉 则调用父类的sum1 i是父类的i 10+10=20
        System.out.println(a.sum1());//20
        //如果把AA的sum2注释掉 则调用父类的sum2和子类的getI(子类的i 100) 100+10=110
        System.out.println(a.sum2());//110
    }
}
class A{
    public int i=10;
    public int getI(){
        return i;
    }
    public int sum1(){
        return i+10;
    }
    public int sum2(){
        return getI()+10;
    }
}
class AA extends A{
    public int i=100;
    public int getI(){
        return i;
    }
    public int sum1(){
        return i+100;
    }
    public int sum2(){
        return getI()+100;
    }
}

多态的应用

多态数组

数组的定义为父类类型 里面保存的实际元素类型为子类类型

创建一个Person对象 两个Student对象 两个Teacher对象 统一放在数组中 调用say和子类特有的方法

public class Test {
    public static void main(String[] args) {
        Person[] persons=new Person[5];
        persons[0]=new Person("jack",20);
        persons[1]=new Student("mary",18,100);
        persons[2]=new Student("smith",19,80);
        persons[3]=new Teacher("scott",30,20000);
        persons[4]=new Teacher("king",50,25000);

        for(int i=0;i< persons.length;i++){
            //persons[i]的编译类型是Person 运行类型根据实际情况判断 动态绑定机制
            System.out.println(persons[i].say());
            if(persons[i] instanceof Student) {//类型判断
                Student student = (Student) persons[i];//向下转型
                student.study();
                //也可以用((Student)persons[i]).study();
            }else if(persons[i] instanceof Teacher) {
                Teacher teacher = (Teacher) persons[i];
                teacher.teach();
            } else if (!(persons[i] instanceof Person)) {
                System.out.println("你的类型有无 请自己检查");
            }
        }
    }
}
class Person{
    private String name;
    private int age;
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public String say(){
        return name+"\t"+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;
    }
}
class Student extends Person{
    private double score;
    public Student(String name,int age,double score){
        super(name,age);
        this.score=score;
    }
    public String say(){//重写方法
        return "学生"+super.say()+" score="+score;
    }
    public void study(){//特有方法
        System.out.println("学生"+getName()+" 正在学java");
    }

    public double getScore() {
        return score;
    }
    public void setScore(double score) {
        this.score = score;
    }
}
class Teacher extends Person{
    private double salary;
    public Teacher(String name,int age,double salary){
        super(name,age);
        this.salary=salary;
    }
    public String say(){//重写方法
        return ""+super.say()+" salary="+salary;
    }
    public void teach(){//特有方法
        System.out.println("老师"+getName()+" 正在讲java");
    }

    public double getSalary() {
        return salary;
    }
    public void setSalary(double salary) {
        this.salary=salary;
    }
}
//jack 20
//学生mary   18 score=100.0
//学生mary 正在学java
//学生smith  19 score=80.0
//学生smith 正在学java
//scott    30 salary=20000.0
//老师scott 正在讲java
//king 50 salary=25000.0
//老师king 正在讲java

多态参数

方法定义的形参类型为父类类型 实参类型允许为子类类型

Object类

equals方法

  • ==: 可以判断基本类型(值是否相等)和引用类型(地址是否相等 即是否是同一个对象)
  • equals:只能判断引用类型 即地址是否相等 子类中往往重写该方法 用于判断内容是否相等
public class Test {
    public static void main(String[] args) {
        String str1=new String("wsx");//false 地址不同
        String str2=new String("wsx");//true 内容相同
        System.out.println(str1==str2);
        System.out.println(str1.equals(str2));
    }
}

重写equals方法

判断两个Person对象的内容是否相等 若各个属性值都相等返回true反之false

public class Test {
    public static void main(String[] args) {

    }
}
class Person{
    private String name;
    private int age;
    private char gender;

    //重写Object类的equals方法
    @Override
    public boolean equals(Object obj) {
        if(this==obj){//若为同一个对象 直接返回true
            return true;
        }
        //类型判断
        if(obj instanceof Person){//是Person才比较
            //向下转型 得到obj的各个属性
            Person p=(Person)obj;
            return this.name.equals(p.name)&&this.age==p.age&&this.gender==p.gender;
        }
        //若不是Person 直接返回false
        return false;
    }

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

    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 char getGender() {
        return gender;
    }
    public void setGender(char gender) {
        this.gender = gender;
    }
}

hashCode方法

  • 返回该对象的哈希码值 可以提高哈希表的性能
  • 实际上 由 Object类定义的 hashCode方法确实会针对不同的对象返回不同的整数(这一般是通过将该对象的内部地址转换成一个整数来实现的 但是Java编程语言不需要这种实现技巧)
  • p352

小结

  1. 提高具有哈希结构的容器的效率
  2. 两个引用 如果指向的是同一个对象 则哈希值肯定是一样的
  3. 两个引用 如果指向的是不同对象 则哈希值是不一样的
  4. 哈希值主要根据地址号来的 不能完全将哈希值等价于地址
  5. hashCode 如果需要的话 也会重写

toString方法

默认返回:全类名+@+哈希值的十六进制
子类往往重写该方法 用于返回对象属性信息
重写该方法 打印对象或拼接对象时 都会自动调用该对象的toString形式
直接输出一个对象时 toString方法会被默认调用 比如System.out.,println(monster);就会默认调用monster.toString()

public String toString() { //重写后,一般是把对象的属性值输出,当然程序员也可以自己定制
    return "Monster{" +
            "name='" + name + ''' +
            ", job='" + job + ''' +
            ", sal=" + sal +
            '}';
}

finalize方法

  • 当对象被回收时 系统自动调用该对象的finalize方法 子类可以重写该方法 做一些释放资源的操作
  • 什么时候被回收:当某个对象没有任何引用时 则jvm就认为这个对象是一个垃圾对象 就会使用垃圾回收机制来销毁该对象 在销毁该对象前 会先调用finalize方法
  • 垃圾回收机制的调用 是由系统来决定(即有自己的GC算法) 也可以通过System.gc()主动触发垃圾回收机制试
  • 在实际开发中 几乎不会运用finalize 所以更多就是为了应付面试

断点调试

image.png

零钱通 房屋出租系统

p362