章节3-面向对象

154 阅读15分钟

方法

方法执行时的内存变化
  • 方法的本质就是将一段具有完整功能的代码块进行封装, 进而将该代码块可以被重复利用
  • 方法只定义,不调用是不会分配内存空间的. (从 Java 8 开始, 方法的字节码文件指令存储在元空间metaspace当中. 元空间使用的是本地内存 静态变量存储在元空间中 )
  • 法调用时会给该方法在JVM的栈内存中分配空间,此时发生压栈动作。这个方法的空间被称为栈帧。
  • 栈帧中主要包括
    • 局部变量表
    • 操作数栈等。
  • 方法执行结束时,该栈帧弹栈,方法内存空间释放。 image.png
方法重载
  • 编译阶段的机制, 在编译阶段已经完成了方法的绑定, 在编译阶段就已经确定了要调用哪一个方法了
  • 在同一个类中, 同样的方法名, 不同的参数, 构成重载
    • 不同参数: 主要是为了让JVM作区分
      • 参数类型不同
      • 参数数量不同
      • 参数顺序不同
方法的递归
  • 后续IO流获取文件目录会用递归

  • 必须要有方法终止执行的条件

  • 在实际开发中, 即使有终止条件, 但由于递归太深, JVM栈内存不够大, 也有可能导致栈溢出, 所以能用循环解决就尽量用循环

  • 问题: 在开发过程中, 项目中由于递归出现栈溢出, 你会怎么解决?

    1. 先尝试调大内存 , 进而判断是否是内存容量过小导致, 同时也可以出判断程序是否有问题
    2. 若调大内存仍无法解决, 大概率是程序出现bug, 这时候再去看代码层面
  • 面试题

  • /**
     * 假如有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,请问第n个月后的兔子有多少对
     * 规律: 当月所生兔子总数 = 上月兔子总数 + 上上月兔子总数(上上月有几只兔子,当月就会生几只兔子)
     * 1 1 2 3
     */
    public class RecursionTest01 {
    
        //递归实现
        public static int sumRabbit2(int month){
            if (month == 1 || month == 2) {
                return 1;
            }
            return sumRabbit2(month - 1) + sumRabbit2(month - 2);
        }
    
        //循环实现
        public int sumRabbit1(int month) {
            int[] ints = new int[month];
            int sum =0;
            ints[1] = 1;
            ints[0] = 1;
            for (int i = 2; i < ints.length; i++) {
                ints[i]= ints[i-1] + ints[i-2];
                sum = ints[i];
            }
            return sum;
        }
    
        @Test
        void test01() {
    //        System.out.println(sumRabbit1(6));
            System.out.println(sumRabbit2(6));
        }
    }
    
  • //使用递归计算n的阶乘 (面试题)
    public class RecursionDemo02 {
    
        public int facTake(int n) {
            if (n == 1) {
                return 1;
            }
            return n * facTake(n - 1);
        }
        @Test
        public void test01(){
            System.out.println(facTake(5));
        }
    }
    
  • 计算1+2+3+4+...n的值 的内存图 image-20241111103430464.png

面向对象

面向对象思想
  • 通过现实现象, 将一部分具有共同特征的事物抽象为一个类. 通过一类事物去实例出类中所属对象. 通过操作对象中的属性与行为去操作数据
对象内存结构
  • 对象存放与堆中, 主要包含成员变量 , 存储成员方法放在元空间, 堆中只会存放对象中方法的地址值, 当调用方法时jvm通过地址值找到方法并进行入栈操作 image-20241111220903882.png
成员变量与局部变量
  • 存储地址不同, 局部变量存储在栈中, 成员变量存储在堆中
  • 作用域不同: 局部变量只能在对应方法中使用 成员变量可以在对应类中的所有方法中使用
  • 生命周期不同: 局部变量随着一个方法的执行结束而结束,也就是一个栈帧出栈后即结束 成员变量由对象的存在而存在
  • 初始化值不同: 成员变量由初始化值, 局部变量没有
this关键字
  • 那个对象调用this this就代表那个对象的地址值, 程序通过this的地址值找到所需要找到的变量

  • 如果没有spring这种思想, 成员变量可以是引用数据类型吗?

    • 答: 可以是 如果成员变量是引用数据类型, 其内存结构与二维数组一致, 成员变量存储对象地址值,指向对应对象
  • 有了spring依赖注入后, 对开发由什么影响 (代码层面)?

    • 在一个类中声明另一个类为成员变量时, book变量默认值为空,是不能直接获取到book对象中的成员变量和成员方法的. 其次, new对象只能在方法中,所以想要使用book中的成员,只能在成员方法中new出Book对象并赋值

    • public class Student {
                      Book book;
                  }
          public void studentDo() {
              book = new Book();  //方法中new对象
              book.readBook();
          }
      }
      
封装
  • 封装是一种思想: 在开发中将对象进行封装处理, 使对象数据私有化, 对于一些不需要暴露的数据和方法进行限制,使调用者无需考虑对象内部实现,只需要关注开放出的接口的功能
  • 封装的实现方式: 权限修饰符
    • private: 只有在本类中的方法可以调用
    • (default): 同一个包中可以调用
    • public: 同一模块中可以调用
    • protected: 同一模块中的子父类
static关键字
  • 当一个属性对象是对象级别的, 这个属性通常定义为成员变量. (成员变量是一个对象一份, 100个对象就应该有100个空间)

  • 当一个属性是类级别的(所有对象都有这个属性, 并且这个属性值是一样的), 建议将其定义为静态变量, 在内存空间中只有一份, 节省内存空间. 这种类级别的属性不需要new对象, 直接通过类名访问

  • public static void main(String[] args) {
        //abs是静态方法
        int abs = Math.abs(-2);
    }
    
  • 静态变量存储在哪里? 静态变量在什么时后初始化?

    • 静态变量存储在堆中( java 8 以后)
    • 静态变量在类加载时初始化(赋初值)
      • 类加载: 将.class文件加载到方法区中
  • 静态变量是在类加载时创建的

继承与多态
/**
 * 继承: 减少代码的重复, 子类方法可以重写.  在我看来就是为多态服务的,方法重写机制就可以将子类抽象后重写方法j
 * 1,Java不能多继承,但可以多层继承
 * 方法的重写:
 * 1,在具有继承关系的类中
 * 2,具有同样的方法名,参数
 * 3,返回值若是引用数据类型,可以是父类返回值的子类 ,例如在父类中返回值是Objet,子类的返回值可以是String
 * 4,权限修饰只能更高,不能更低
 * 4,静态变量,构造方法,私有变量不可以被继承
 */
public class OverrideTest {
    public static void main(String[] args) {
        /**
         * 多态:
         * 程序的执行分为两个阶段
         *  1: 编译阶段--->静态绑定
         *      当程序执行前, 会先编译, 当程序进行编译时,会根据当前代码进行初步地校验程序是否可以执行, 比如anilam.eat(); 程序编译时
         *      会先到Anulam类中寻找是否存在eat()方法,若存在,则编译通过.
         *  2: 运行阶段---->动态绑定
         *      当程序运行时,JVM会根据引用变量的地址值去寻找对应的调用者,Anilam anilam = new Dog(); 此代码是将堆中dog对象的地址值
         *      引用到anilam变量中, 所以程序运行后,会通过地址值获取到dog对象的eat()方法进行执行
         *
         *  由于程序由于不同的执行状态,所以因为这种执行状态的不同,造成不同的执行结果,称之为多态.
         *
         *  静态方法与静态变量不能多态! 静态方法与静态变量可以被继承,但不能被重写. 而且静态是类          *  级别的,是通过类名调用的,不需要通过对象引用寻找具体方法
         *
         *
         */

//      向上转型
        Anilam anilam = new Dog();
        anilam.eat();

        /**
         * 由于继承的特性, 也就是有继承关系的两个类,就可以进行向下转型. 由于编译阶段只会判断强制转换的两个类是否有继承关系,
         * 若有,则编译认为语法无异常. 但到运行阶段, jvm拿着anilam变量中引用的dog对象的地址值去调用cat对象中的catJob()方法,是找不到的
         *
         * 如此测诞生了instanceof关运算符
         *  作用: 将变量中引用的的地址值与对象判断,是否有继承关系
         */
//      ClassCastException:类型转换异常
        if (anilam instanceof Cat){
            Cat cat = (Cat) anilam;
            cat.catJob();
        }else if (anilam instanceof Dog){
//          向下转型
            Dog dog = (Dog) anilam;
            dog.dogJob();
        }

    }
}
多态的作用
  • 降低程序的耦合度,提高程序的扩展力。

  • 尽量使用多态,面向抽象编程,不要面向具体编程。

  • class People {
        public void feed(Cat cat){
            cat.eat();
        }
    }
    
    class Cat {
        public void eat(){
            System.out.println("猫吃老鼠");
        }
    }
    
    class Dog {
        public void eat(){
            System.out.println("狗吃骨头");
        }
    }
    class Test{
        public static void main(String[] args) {
            People people = new People();
            people.feed(new Cat());
            /**
             *若此时人类想要喂养其他动物时, 则需要在People中添加feed(Dog dog)方法. 由于OCP开闭原则, 功能的修改或关闭不修改原代码
             * 所以此时程序设计是落后的,耦合度很高
             */
    //        people.feed(new Dog());
        }
    }
    
  • package com.liny.duotai;
    
    class People {
        /**
         * people.feed(new Dog()); 此时参数构成了多态, feed()方法传入了一个Dog对象, 引用给Zoon, 这便是多态的作用: 通过抽象子类, 将程序解耦!
         * @param zoon
         */
        public void feed(Zoon zoon){
            zoon.eat();
        }
    }
    
    class Cat extends Zoon{
        public void eat(){
            System.out.println("猫吃老鼠");
        }
    }
    
    class Dog extends Zoon{
        public void eat(){
            System.out.println("狗吃骨头");
        }
    }
    
    /**
     * 面向抽象编程, 不要面向具体编程!!!
     * 
     * 猫,狗,或其他更多的宠物都属于动物, 我们可以将这种具体个体抽象出更高一类, 使用更高类作为参数进行处理, 若以后再有动物添加(比如鸟儿),
     * 只需要将鸟儿继承动物类即可
     */
    class Zoon{
        public void eat(){};
    }
    
    class Test{
        public static void main(String[] args) {
            People people = new People();
            people.feed(new Cat());
            /**
             *若此时人类想要喂养其他动物时, 则需要在People中添加feed(Dog dog)方法. 由于OCP开闭原则, 功能的修改或关闭不修改原代码
             * 所以此时程序设计是落后的,耦合度很高
             */
           people.feed(new Dog());
        }
    }
    
软件开发七大原则
  • 概念: 软件开发原则旨在引导软件行业的从业者在代码设计和开发过程中,遵循一些基本原则,以达到高质量、易维护、易扩展、安全性强等目标。软件开发原则与具体的编程语言无关的,属于软件设计方面的知识。
    • ==开闭原则== (Open-Closed Principle,OCP):一个软件实体应该对扩展开放,对修改关闭。即在不修改原有代码的基础上,通过添加新的代码来扩展功能。(最基本的原则,其它原则都是为这个原则服务的。)
    • 单一职责原则:一个类只负责单一的职责,也就是一个类只有一个引起它变化的原因。
    • 里氏替换原则:子类对象可以替换其基类对象出现的任何地方,并且保证原有程序的正确性。
    • 接口隔离原则:客户端不应该依赖它不需要的接口。
    • 依赖倒置原则:高层模块不应该依赖底层模块,它们都应该依赖于抽象接口。换言之,面向接口编程。
    • 迪米特法则:一个对象应该对其它对象保持最少的了解。即一个类应该对自己需要耦合或调用的类知道得最少。
    • 合成复用原则:尽量使用对象组合和聚合,而不是继承来达到复用的目的。组合和聚合可以在获取外部对象的方法中被调用,是一种运行时关联,而继承则是一种编译时关联。
面向对象中的关键字
  • 封装
    1. 权限修饰符
    2. static: 表明具有静态属性
  • 继承
    1. extends: 表明一个类型是另一个类型的子类型。对于类,可以是另一个类或者抽象类;对于接口,可以是另一个接口
    2. abstract: 表明类或者成员方法具有抽象属性
    3. final: 用来说明最终属性,表明一个类不能派生出子类,或者成员方法不能被覆盖,或者成员域的值不能被改变,用来定义常量
    4. this: 指向当前实例对象的引用
    5. super: 用于表示父类结构的标识符
      • 接口
        1. interface: 声明为接口类型
        2. default: 默认,例如,用在switch语句中,表明一个默认的分支。Java8 中也作用于声明接口函数的默认实现
        3. implements: 表明一个类实现了给定的接口
  • 多态
    1. instanceof: 用来测试一个对象是否是指定类型的实例对象
抽象类与接口
  • 抽象类

    • 内部结构
      1. 有构造器, 但无法new对象 (原因: 构造器中存在抽象方法, 抽象方法没有方法体, 若抽象类可以new对象, 当类加载时, 无法知道需要创建具体大小的内存空间)
      2. abstract关键字不能和private,final,static关键字共存。
  • 接口

    • 内部结构
      1. 无构造器, 无法new对象
      2. 接口中只能定义:常量+抽象方法。接口中的常量的static final可以省略。接口中的抽象方法的abstract可以省略。接口中所有的方法和变量都是public修饰的。
      3. Java8之后,接口中允许出现默认方法和静态方法(JDK8新特性)
  • 共同点

    • 子类或实现都必须重写或实现父类中所有的抽象方法(==只需要重写抽象方法, 其他没有强制要求==)
  • 接口与抽象类如何选择

    • 抽象类主要适用于公共代码的提取。当多个类中有共同的属性和方法时,为了达到代码的复用,建议为这几个类提取出来一个父类,在该父类中编写公共的代码。如果有一些方法无法在该类中实现,可以延迟到子类中实现。这样的类就应该使用抽象类。
    • 接口主要用于功能的扩展。例如有很多类,一些类需要这个方法,另外一些类不需要这个方法时,可以将该方法定义到接口中。需要这个方法的类就去实现这个接口,不需要这个方法的就可以不实现这个接口。接口主要规定的是行为。
匿名内部类
  • 特点

    • 特殊的局部内部类,没有名字,只能用一次。
    • 匿名内部类只是表征, 其匿名对象的引用就是在方法的参数上-->(Inter inter)
 public  class InnerDemo  {
     public static void main(String[] args)  {
         useInter(new Inter(){
             @Override
             public void show() {
                 System.out.println("匿名内部类的输出1");
             }
         });
         
         //补全写法
         useInter((Inter) new Inter() {
             @Override
             public void show() {
                 System.out.println("匿名内部类的输出2");
             }
         });
     }
 ​
     /**
      * @param inter 测试接口,不可以new对象
      */
     public static void useInter(Inter inter){
         inter.show();
     }
 }
Lambda表达式
  • 使用前提条件

    1. Lambda必须是接口类型。
    2. 接口中有且仅有一个抽象方法。
  • 简化方式

    1. 任何情况下,参数类型可以省略
    2. 如果参数列表有且仅有一个参数,()可以省略;
    3. 如果方法体中有且仅有一条语句,那么{}和;一起省略,如果有return,return也要一起省略。
  • Lambda表达式的好处:

    • 简化匿名内部类的书写。
  • Lambda表达式和匿名内部类的区别?

    1. 能使用Lambda表达式的地方就可以使用匿名内部类, 能使用匿名内部类的地方不一定可以使用Lambda表达式。
    2. 匿名内部类编译之后有class文件,Lambda表达式编译之后没有class文件。
  • 案例:

  •          package com.itheima.inner;
             interface Inter {
                 void show();
             }
             ​
             /**
              * 内部类
              */
             public class InnerDemo {
                 public static void main(String[] args) {
                     useInter(new Inter() {
                         @Override
                         public void show() {
                             System.out.println("匿名内部类的输出");
                         }
                     });
             ​
                     //补全写法
                     useInter((Inter) new Inter() {
                         @Override
                         public void show() {
                             System.out.println("匿名内部类的输出2");
                         }
                     });
                     
                     useInter(() -> System.out.println("Lambda表达式的输出"));
                     
                     //直接调用接口方法
                     ((Inter) ()-> System.out.println("Lambda表达式的输出")).show();
                 }
                 
                 /**
                  * @param inter 测试接口,不可以new对象
                  */
                 public static void useInter(Inter inter) {
                     inter.show();
                 }
             }
    
总结
  • 从封装开始, 将现实中的事物封装为对象, 通过操作对象,实现想要的功能, 此时还是面向具体编程. 后续学习静态的概念, 减少了代码的复用, 引入了共享数据的概念. 以此学习继承, 从继承中学习到子类可以继承父类中的属性与方法, 对代码进一步的减少复用. 且有了方法的重写. 为了代码的解耦,开始进入多态的时代(父类的引用指向子类) . 通过重写, 抽象两大概念, 多态成功对代码进行解耦, 将具体事物进行抽象 ,使用多态将父类的引用获取具体实物的对象进行数据操作.