封装

简介

封装就是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(方法),才能对数据进行操作。
Java核心技术的解释 —— 将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式(比如调用Math.random,不必了解如何具体实现)。对象中的数据称为实例字段(比如String name等),操作数据的过程称为方法。实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。

好处

  1. 隐藏实现细节,使用者只需要调用即可。类似的,电视机也是一种封装,用户想要开机等操作,只需要在遥控器上按一下就可以了,不用知道电视机内部进行的复杂操作
    2. 可以对要进行赋值的数据进行检验,保证安全合理。
    1. int age = 12000; //这就是没有封装的坏处,直接就对一个变量赋值,没有对赋值的数据进行检验

    封装步骤

    1. 将属性进行私有化 private,这样就不能直接修改属性了。
    2. 提供一个公共的(public) set方法,用于检验数据并给属性赋值。
    1. public void setXxx(类型 参数名){
    2. //加入数据验证的业务逻辑
    3. 属性 = 参数名;
    4. }
    3. 提供一个公共的 get方法,用于获取属性的值。
    1. public 数据类型 getXxx(){
    2. return Xxx;
    3. }

    封装与构造器

    为了防止调用构造器绕过set函数,我们可以把set函数写在构造器中。
    1. public Person(String name,int age,double salary){
    2. this.setName(name);
    3. this.setAge(age);
    4. this.setSalary(salary);
    5. } //在构造器中写入封装方法
    image.png

    继承

    基本介绍

    继承可以解决代码复用,让我们的编程更加靠近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends来声明继承父类即可

    示意图

    image.pngimage.png

    基本语法

    1. class 子类 extends 父类{}
  2. 子类会自动拥有父类定义的属性和方法(当然受访问修饰符的限制)。
    2. 父类又叫超类,基类。
    3. 子类又叫派生类。
    4. 假设A继承B,B继承C,那么C也算A的父类。

    注意事项

  3. 子类继承了父类所有的属性和方法,非私有的属性和方法可以在子类直接访问,但是私有属性和方法不能在子类直接访问,要通过父类提供公共的方法去访问
    2. 子类必须调用父类的构造器,完成父类的初始化。
    3. 当创造子类对象时,不管使用子类哪个构造器,默认情况下都会去调用父类的无参构造器,相当于子类的构造器有一个默认语句 super()。 ```java public class Pupil extends Person{
    double score;
    Pupil(){
    1. System.out.println("调用pupil构造器");
    }
    public static void main(String[] args) {
    1. Pupil pupil = new Pupil(); // 调用子类Pupil的构造器
    } }
  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/23175776/1641723937499-f06fc310-ee4e-4f25-9402-6468858ba3a2.png#clientId=u5b1043af-a010-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u70011396&margin=%5Bobject%20Object%5D&name=image.png&originHeight=195&originWidth=940&originalType=url&ratio=1&rotation=0&showTitle=false&size=22525&status=done&style=none&taskId=ufc9af0b1-14ff-458a-abea-f8e5852ee21&title=)<br />**发现首先调用的是父类的构造器,然后再调用子类的构造器。**<br />4. **如果父类没有提供无参构造器,则必须在子类的构造器中用super指定使用父类的某个构造器完成对父类的初始化工作,否则编译不会通过**。
  2. ```java
  3. Person(int age,String name){ //父类的构造函数,有参数
  4. this.age = age;
  5. this.name = name;
  6. }
  7. public class Pupil extends Person{
  8. //继承的子类
  9. double score;
  10. Pupil(){
  11. System.out.println("调用pupil构造器");
  12. }
  13. public static void main(String[] args) {
  14. Pupil pupil = new Pupil(); //直接报错,因为没有调用父类的构造器
  15. }
  16. }

image.png

  1. Pupil(){
  2. super(10,"wang");
  3. System.out.println("调用pupil构造器");
  4. } //正确,用super调用父类构造器
  1. 如果希望指定去调用父类的某个构造器,则显式的调用一下:super(参数列表)。
    6. super在使用时,必须放在构造器第一行( super() 调用构造器这种方法只能在构造器中使用)
    1. Pupil(){
    2. System.out.println("调用pupil构造器");
    3. super(10,"wang"); //报错,super调用构造器必须放在第一行
    4. }
    7. super() 和 this() 都是调用构造器的方法,都必须放在构造器的第一行,因此这两个方法不能共存在一个构造器
    8. Java中所有类都是Object类的子类(可以用ctrl + H查看)。
    image.png
    9. 调用子类构造器时,会向上一直调用父类的构造器,一直追溯到Object类。
    image.png
    1. public static void main(String[] args) {
    2. Pupil pupil = new Pupil(); //从上往下开始调用构造器
    3. }
    image.pngimage.png
    10. 子类最多只能继承一个父类(直接继承,删除了C++的多重继承),Java中是但继承机制。那么任何让A类同时继承B类和C类? A 继承 B,B 继承 C
    11. 不能滥用继承,子类和父类之间必须满足 “is - a”(也就是必须有共同点)的逻辑关系。
    1. Music extends Person //不合理
    2. Cat extends Animal //合理

    本质分析

    分析当子类继承父类,创建子类对象时,内存中到底发生了什么。
    image.pngimage.pngimage.png
    1. Son son = new Son();
    2. System.out.println(son.name); //本类中有该信息,返回 "小头儿子"
    3. System.out.println(son.age); //父类中有该信息,返回 39
    4. System.out.println(son.hobby); //父类的父类有该信息,返回 "旅游"
    要按照查找关系来返回信息
    1. 首先看子类是否有该属性,如果子类有这个属性,则返回信息(因为是在本类中,因此绝对可以访问,即使是private)
    2. 如果子类没有这个属性,就看父类有没有这个属性,如果父类有该属性,并且可以访问,就返回信息,如果该信息不可访问(private),那么就直接报错(can not access),不会再向上查找了
    image.png
    3. 如果父类没有该信息,那么就按照 (2) 的规则,继续找上级,直到Object,如果到最顶端也没有查找到,则提示该方法不存在。

多态

对于解决某些问题,比如 假设需要创建一个方法给宠物喂食,那么用传统方法来说,给猫喂鱼,狗喂骨头等等,需要每一个都写一个方法这样导致代码的复用性不高,而且不利于代码的维护,因此需要多态
多态指 方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承基础之上的。

方法的多态

重写和重载就体现了多态。

  1. // 方法重载体现了多态——在同类中方法有多种形态
  2. public int sum(int num1,int num2){
  3. return num1 + num2;
  4. }
  5. public int sum(int num1,int num2,int num3){
  6. return num1+num2+num3;
  7. }
  8. // 方法重写体现了多态——在不同类中方法也有多种形态
  9. public int sum(int num1,int num2)
  10. {
  11. return num1 + num2;
  12. } //父类方法
  13. public int sum(int num1,int num2)
  14. {
  15. return num1 - num2;
  16. } //子类方法

对象的多态

  1. 一个对象的编译类型和运行类型可以不一致编译类型看定义时 =号 的左边,运行类型看 =号 的右边。
    1. Animal animal = new Dog(); // animal 编译类型是Animal,运行类型为 Dog
    2. 编译类型在定义对象时就确定了,不能改变。
    3. 运行类型是可以变化的。
    1. animal = new Cat(); // animal的运行类型变成了Cat,编译类型仍然是Animal
    4. 对象多态的前提:两个对象(类)之间存在继承关系。
    因为对象的多态,很多操作就简洁许多了。以上面的喂食例子,我们正常写的话应该是这样:
    1. public void feed(Cat cat, Fish fish){
    2. ...; //输出喂食信息
    3. }
    4. public void feed(Dog dog, Fish fish){
    5. ...; //输出喂食信息
    6. }
    7. ...... //所有动物类与食物类都要写一遍,写在Master类中
    而如果用对象的多态方法来写,则是下面这样:当调用时,仍然是传入Animal类和Food类的子类,这时就用到了多态机制。
    1. public void feed(Animal animal, Food food){
    2. ...;
    3. } // 只需在Master类中写一个即可
    5. 多态参数:方法定义的形参类型为父类类型,实参类型允许为子类类型。

    向上转型

    1. 本质:父类的引用指向了子类的对象。
    2. 语法:父类类型 引用名 = new 子类类型();
    3. 特点:
    1)编译类型看左边,运行类型看右边。
    2)可以调用父类中的所有成员(当然前提是遵守访问权限)。
    3)不能调用子类中特有属性和方法(因为在编译阶段,能调用哪些成员,是由编译类型来决定的。如果子类重写了父类的方法,那么由于动态绑定机制,调用父类的成员和方法会先从子类找,因此子类重写的方法是可以被调用的。但是如果子类重写了父类的属性,那么调用看的还是编译类型,因为属性没有动态绑定机制。 总结:调用重名属性看编译类型,调用重名方法看运行类型。 ```java public class Car { public int age; public void say(){
    1. System.out.println("这是一个car类");
    } }

class BMW extends Car{ public int age = 10; //同名属性 public void say(){ //同名方法 System.out.println(“这是一个BMW类”); } public void hi(){ //独特方法 System.out.println(“hello”); } public static void main(String[] args) { Car car = new BMW(); Car car1 = new Car(); BMW bmw = new BMW(); System.out.println(car.age); //0 编译类型为Car System.out.println(car1.age); //0 System.out.println(bmw.age); //10 car.say(); //BMW类 运行类型为BMW car.hi(); //报错,编译类型是Car,不能调用BMW的独特方法 car1.say(); //Car类 bmw.say(); //BMW类 } }

  1. ```java
  2. Animal animal = new Cat();
  3. animal.catchMouse(); //错误,因为catchMouse是子类的特有方法,向上转型不能调用

向下转型

1. 语法:子类类型 引用名 = (子类类型) 父类引用。

  1. Cat cat = (Cat)animal;

这里注意一点,向下转型后,cat的编译类型和运行类型都是Cat,cat和animal都指向Cat的对象(注意animal没有消失)。
2. 只能强转父类的引用,不能强转父类的对象。父类的对象是创建在堆区中的,这个是不能改变的(因为已经创建了),但是可以改变指向该对象的指针,向下转型就是让它指向一个子类的对象
3. 要求父类的引用必须指向的是当前目标类型的对象。

  1. Animal animal = new Cat();
  2. Dog dog = (Dog)animal; //报错
  3. Cat cat = (Cat)animal; //正确

就比如这个例子,父类的引用本来就指向Cat类的对象,因此向下转型只能使用Cat按照错误语句的理解,让一只狗指向猫对象,那肯定是错误的
4. 当向下转型后,可以调用子类类型中所有的成员(当然要符合访问范围)。
5. 下面是错误的向下转型写法,不能让没有引用的对象进行向下转型。

  1. Cat cat = (Cat)(new Animal());
  2. Cat cat = (Cat)new Animal();
  3. // 这两种写法都是错误的,不能让没有引用的对象进行向下转型
  4. // 编译器报告 cannot be cast to 错误

多态数组

数组的定义类型为父类类型,里面保存的实际元素类型为子类类型,静态初始化可以直接写,动态初始化则需要new父类,然后对里面的元素单个向上转型。

  1. Person[] a = new Student[3]; //不能这样写
  2. // 第一种写法,静态初始化
  3. Preson p = new Person();
  4. Person t = new Teacher();
  5. Person s = new Student();
  6. Person[] persons = {p,t,s};
  7. //第二种写法,动态初始化
  8. Person[] a = new Person[3]; //new父类
  9. a[0] = new Student();

这里需要介绍一个问题:如何调用子类特有的方法(很明显,光有向上转型是不能调用的,向上转型只能让子类加入到父类的数组中,因此需要用到向下转型

  1. public class Person {
  2. private int age;
  3. private String name;
  4. }
  5. public class Student extends Person{
  6. public void Study(){
  7. System.out.println("学生正在学习");
  8. }
  9. }
  10. public class Test {
  11. public static void main(String[] args) {
  12. Person[] a = new Person[3];
  13. a[0] = new Student(); //向上转型
  14. a[0].Study();
  15. //报错,因为Study是子类特有的方法,向上转型不能调用(调用方法由编译类型决定)
  16. }
  17. }
  18. public class Test {
  19. public static void main(String[] args) {
  20. Person[] a = new Student[3];
  21. a[0] = new Student();
  22. Student stu = (Student)a[0]; //向下转型
  23. stu.Study();// 语义相同:((Student) a[0]).Study();
  24. }
  25. }

房屋出租小项目的思路

image.pngimage.png

Java的动态绑定机制

1. 当调用对象方法的时候(不管是直接调用还是方法里调用),该方法会和该对象的内存地址(运行类型)绑定,根据运行类型进行调用,如果没有该方法,就启用继承机制。
2. 当在方法中使用对象属性时,没有动态绑定机制,调用哪个类的方法,就用哪个类的属性,如果没有就启用继承机制。用一个例子解释清楚:

  1. public class Computer {
  2. public int i = 10;
  3. public int sum(){
  4. return getI() + 10;
  5. }
  6. public int sum1(){
  7. return i + 10;
  8. }
  9. public int getI(){
  10. return i;
  11. }
  12. }
  13. public class NotePad extends Computer{
  14. public int i = 200;
  15. public int sum(){
  16. return i + 100;
  17. }
  18. public int getI(){
  19. return i;
  20. }
  21. public int sum1(){
  22. return i + 200;
  23. }
  24. }

父类和子类的成员完全相同,只不过内容有变化,注意sum不是子类的特有方法,因此向上转型之后可以调用

  1. public static void main(String[] args) {
  2. Computer a = new NotePad(); //向上转型,运行类型为 NotePad
  3. System.out.println(a.sum()); //输出 300,用的子类的方法和子类的i
  4. System.out.println(a.sum1()); //输出 400
  5. System.out.println(a.i); //输出 10,为父类的i
  6. }

可以发现,由于运行类型是NotePad,当调用对象方法的时候,该方法会和该对象的内存地址(运行类型)绑定,因此调用的方法直接从子类开始找,由于当在方法中使用对象属性时,没有动态绑定机制,调用哪个类的方法,就用哪个类的属性。子类的sum方法用到了 i,根据这条定理,应该用子类的 i。但是注意一点,如果直接调用a的i,由于属性没有重写,调用属性要看编译类型,因此使用父类的 i。
然后把子类的sum方法和sum1方法全部删除:

  1. public class NotePad extends Computer{
  2. public int i = 200;
  3. public int getI(){
  4. return i;
  5. }
  6. }
  7. public class Test {
  8. public static void main(String[] args) {
  9. Computer a = new NotePad();
  10. System.out.println(a.sum()); //输出210,父类的sum方法调用的是子类的getI方法 System.out.println(a.sum1()); //输出20,用父类的i
  11. }
  12. }

当把子类的sum方法和sum1方法都删除后,调用这些方法就要启动继承机制了(因为子类没有这两个方法),在父类中寻找这两个方法。注意父类的sum方法调用了一个 getI 函数在方法里调用的方法也和运行类型绑定,因此调用的是子类的getI方法,返回的是子类的i,因此结果为210,而父类的sum1方法调用了属性 i,由于属性没有动态绑定机制,因此调用自己的 i,结果为20。
再次强调,属性没有重写之说,调用哪个属性要看编译类型。

  1. public class Computer {
  2. String name = "我是一台电脑"; //在父类定义一个属性
  3. }
  4. public class PC extends Computer{
  5. String name = "我是一台PC"; //在子类定义一个相同名字的属性
  6. }
  7. public class Test {
  8. public static void main(String[] args) {
  9. Computer c1 = new Computer(); //编译类型为Computer
  10. Computer c2 = new PC(); //向下转型,编译类型为Computer
  11. PC c3 = new PC(); //编译类型为PC
  12. PC c4 = (PC)new Computer(); //向上转型,编译类型为PC
  13. System.out.println(c1.name);
  14. System.out.println(c2.name);
  15. System.out.println(c3.name);
  16. System.out.println(c4.name);
  17. }
  18. }

image.pngimage.png 调用属性与编译类型保持一致。

单例设计模式

设计模式

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式就像是经典的棋谱,不同的棋局,我们用不同的棋谱,免去我们自己再思考和摸索。

单例设计模式

采取一定方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法
单例模式有两种方式:1. 饿汉式 2. 懒汉式

饿汉式

1. 构造器私有化——防止直接new出来一个对象。
2. 类的内部创建一个静态对象。
3. 向外暴露一个静态的公共方法,该方法返回一个静态对象。

  1. public class GirlFriend {
  2. private String name;
  3. private static GirlFriend gf = new GirlFriend("小红"); //内部创建的对象
  4. public static GirlFriend getInstance(){
  5. return gf;
  6. } //对外公开的获得对象方法
  7. private GirlFriend(String name) {
  8. this.name = name;
  9. } //构造器私有化
  10. }
  11. public class Test {
  12. public static void main(String[] args) {
  13. GirlFriend gf1 = GirlFriend.getInstance();
  14. GirlFriend gf2 = GirlFriend.getInstance(); //都返回 gf
  15. if(gf1 == gf2){
  16. System.out.println("同一个人"); //输出 同一个人
  17. }
  18. }
  19. }

注:饿汉式在内部就创建了对象,如果不使用的话会造成内存空间的浪费。

懒汉式

  1. 仍然构造器私有化。
    2. 定义一个static静态属性对象(注意不new)。
    3. 提供一个public的static方法,可以返回一个static对象。
    4. 当用户第一次使用getInstance方法时,才会创建static对象,再次调用就返回那个静态对象。
    1. public class Person {
    2. private static Person aa; //私有经典对象
    3. private Person(){
    4. System.out.println("构造器调用");
    5. }
    6. private static Person getInstance(){
    7. if(aa == null){
    8. aa = new Person(); //调用时才分配空间
    9. }
    10. return aa;
    11. }
    12. }

    区别

    1. 二者最主要的区别在于创建对象的时机不同:饿汉式是在类加载之前就创建了对象实例,而懒汉式是在使用时才创建。
    2. 饿汉式不存在线程安全问题,懒汉式存在线程安全问题。
    3. 饿汉式存在浪费资源的可能。因为如果程序员一个对象实例都没有使用,那么饿汉式创建的对象就浪费了,懒汉式是使用时才创建,就不存在这个问题。
    4. 在JavaSE标准类中,java.lang.Runtime就是经典的单例模式。

instanceof比较操作符

instanceof 是 比较操作符,用于判断对象的运行类型是否为XX类或XX类的子类型。
语法是 实例对象 instanceof 类。

  1. public static void main(String[] args) {
  2. Computer c1 = new Computer();//运行类型为Computer
  3. Computer c2 = new PC(); //运行类型为PC
  4. System.out.println(c1 instanceof Computer); //true
  5. System.out.println(c1 instanceof PC); //false
  6. System.out.println(c2 instanceof Computer); //true
  7. System.out.println(c2 instanceof PC); //true
  8. }

image.png