Java基础学习(六)

Java基础学习(六):面向对象

本文为个人学习记录,内容学习自 狂神说Java黑马程序员


概念

  1. 面向过程 vs 面向对象
    • 面向过程
      • 步骤清晰简单,第一步做什么,第二步做什么,...
      • 适合处理一些较为简单的问题
    • 面向对象
      • 分类的思维模式,思考解决该问题需要哪些分类,然后对这些分类进行单独思考
      • 分类下的具体细节是面向过程的
      • 适合处理复杂的问题
  2. 什么是 面向对象
    • 面向对象编程(Object-Oriented Programming, OOP)
    • 面向对象编程的本质是:以类的形式组织代码,以对象的形式组织(封装)数据
    • 核心思想:抽象
    • 三大特性:封装、继承、多态

类的结构

类包含了 属性方法 两部分:

public class Student {
    // 属性
    String name;
    int age;

    // 方法
    public void study(){
        // 方法实现
    }
}

对象的创建与初始化

  • 使用 new 关键字创建对象 Student student = new Student();

  • 使用 new 关键字创建的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始化,以及对类中 构造器 的调用

  • 构造器 也称为构造方法,是在创建对象时必须调用的,有以下特点:

    • 必须和类的名字相同
    • 必须没有返回类型,也不能写 void
    • 构造方法用于初始化,包括 无参构造有参构造
    • 如果没有显式定义构造方法,会自动生成一个包含空语句的无参构造
    • 如果定义了有参构造但没有定义无参构造,是不会自动生成无参构造的,必须完全没有显式定义才会自动生成
    • 快捷键:Alt + Insert
    public class Person {
        String name;
        // 无参构造
        public Person(){
            this.name = "victoria";
        }
        // 有参构造
        public Person(String name){
            this.name = name;
        }
    }
    
  • 在类中,和类同名的方法如果没有返回值则会被识别为构造方法,如果有返回值则会被识别为成员方法


内存分析

下面通过例子介绍一个基本程序的内存分配过程:

public class Application {
    public static void main(String[] args) {			// main方法作为程序入口
        Pet dog = new Pet();							// 创建对象
        dog.name = "a";
        dog.age = 3;
        dog.shout();
    }
}
class Pet {
    String name;
    int age;
    public void shout(){}
}

基本步骤:

  1. 在方法区中加载 Application 类,存储了 main 方法的具体实现,并将常量添加到常量池中;
  2. 在栈中创建一个栈帧用于执行 main 方法;
  3. 由于需要用到 Pet 类,因此在方法区中加载 Pet 类,存储了成员变量的元信息(包括名称、类型等)以及方法的具体实现;
  4. 执行 Pet dog = new Pet();时,先在栈中分配引用变量 dog 的空间,用于存储地址;
  5. 之后在堆中分配内存存储该对象的具体信息,name 初始化为 null,age 初始化为0;
  6. 通过引用变量找到堆中具体地址进行赋值,并根据方法表定位到 shout() 在方法区中的具体实现;
  7. main 方法执行完毕后栈帧被弹出,回收不再使用的内存。

内存空间迭代过程:在 JDK6 及之前,常量池和静态区都位于方法区中;在 JDK7 时,将常量池中的字符串常量池迁移到了堆中;在 JDK8 时,将静态区中的静态变量迁移到了堆中,方法区更名为元空间

图6-1

封装

  1. 公有(public)和私有(private)

    成员变量/方法通过 public 或者 private 修饰,表明该成员变量/方法能否被实例对象访问

    public class Application {
        public static void main(String[] args) {
            Student s1 = new Student();
            s1.name = "victoria";							// name是公有的,可访问
            s1.id = 1;										// id是私有的,不可访问,此处会报错
        }
    }
    class Student {
        public String name;
        private int id;
    }
    
  2. get / set

    • 为了防止外部随意改动,一般将类的成员变量设置成私有的,而为了能够读写成员变量,一般要提供公有的 get 方法和 set 方法

    • 快捷键:Alt + Insert

    public class Application {
        public static void main(String[] args) {
            Student s1 = new Student();
            String name = s1.getName();
            s1.setName("victoria");
        }
    }
    class Student {
        private String name;								// 成员变量设置成私有的
        public String getName(){							// get方法,用于获取私有成员变量的值
            return this.name;
        }
        public void setName(String name){					// set方法,用于设置私有成员变量的值
            this.name = name;
        }
    }
    

继承

  • 使用关键字 extends 表明继承关系:public class Student extends Person{} —— 表明 Student 类继承自 Person 类

  • Student 类继承自 Person 类,则 Student 类称为子类/派生类,Person 类称为父类/基类

  • 子类能继承父类的哪些内容?

    首先需要明确:能继承 ≠ 能访问,子类无法直接访问父类中的私有成员变量,但实际上是会继承下来的

    构造方法 非私有 × 私有 ×
    成员变量 非私有 √ 私有 √
    成员方法 虚方法表 √ 否则 ×
  • 方法的继承与虚方法表:为了提高方法继承的效率,在 Java 中,将(非private && 非static && 非final)的方法称为虚方法,父类会将自身的虚方法存储在虚方法表中,子类在继承时会在父类的基础上在虚方法表中添加自己的虚方法,从而在多级继承时提高效率

  • 虽然使用 private 修饰的方法、使用 static 修饰的方法、使用 final 修饰的方法都不会被继承,但在发生方法重写时存在不同:使用 private 修饰的方法在子类中可以出现同名并且同形参的方法,但不会被视为重写;使用非 private 修饰的 final 方法不能在子类中出现同名并且同形参的方法,会报错;使用非 final 修饰的 static 修饰方法在子类中可以出现同名并且同形参的方法,但不会被视为重写;总而言之,private 优先级最高,只要有 private 那一定不报错,接着看 final,如果没有 private 有 final 就会报错,最后看 static,在满足上述两条规则的情况下,如果有 static 也不会报错,但也不会被视为重写

  • 特殊情况:对于跨包(不在同一个包)的子类,使用 default 修饰的方法就算满足非 static 非 final 的条件也是不会被子类写进自身的虚方法表的,也即无法继承,这一点和访问修饰符的权限等级是相印证的

  • 继承时的内存分析:子类继承父类的成员变量时,可以理解成堆中存在连续的两个区域分别存储子类自身的成员变量和子类从父类继承下来的成员变量,当访问成员变量时根据“就近原则”先在子类自身的成员变量中找,再在继承下来的成员变量中找;子类继承父类的方法时,在虚方法表中存储了虚方法的地址,方法的实现都存储在方法区中

  • 就近原则

    当类的方法中存在和成员变量同名的局部变量时,优先使用局部变量

    优先级:局部变量 > 子类成员变量 > 父类成员变量

    System.out.println(name);					// 按 局部变量 -> 子类成员变量 -> 父类成员变量 的顺序查找
    System.out.println(this.name);				// 按 子类成员变量 -> 父类成员变量 的顺序查找
    System.out.println(super.name);				// 在 父类成员变量 中查找
    
  • Java 中所有的类默认继承自 Object 类

  • Java 中类只有单继承,没有多继承(接口可以多继承):一个父类可以有多个子类,但一个子类只能有一个父类

  • thissuper

    this 指代当前类,super 指代直接父类,通过 this.属性/this.方法名() 访问当前类的属性/方法,通过 super.属性/super.方法名() 访问直接父类的属性/方法

    本质:this 的本质是方法调用者(当前对象)的地址

    关键字 访问成员变量 访问成员方法 访问构造方法
    this this.成员变量 this.成员方法(...) this(...)
    super super.成员变量 super.成员方法(...) super(...)
    public class Application {
        public static void main(String[] args) {
            Student student = new Student();
            student.test();
        }
    }
    class Person {
        protected String name = "a";						// 父类中的成员变量name初始化为"a"
    }
    class Student extends Person{
        private String name = "b";							// 子类中的成员变量name初始化为"b"
        public void test(){
            System.out.println(name);						// 根据就近原则,访问的是子类本身的成员变量,输出为"b"
            System.out.println(this.name);					// 使用this访问子类本身的成员变量,输出为"b"
            System.out.println(super.name);					// 使用super访问从直接父类继承的成员变量,输出为"a"
        }
    }
    
  • 构造方法的执行:执行子类构造方法时默认调用了父类的无参构造

    public class Application {
        public static void main(String[] args) {
            Student student = new Student();				// 创建子类实例
        }
    }
    class Person {
        public Person(){									// 父类构造方法
            System.out.println("执行父类构造方法");
        }
    }
    class Student extends Person{
        public Student(){									// 子类构造方法
            System.out.println("执行子类构造方法");
        }
    }
    ===============================================================================
    输出结果:
    执行父类构造方法
    执行子类构造方法
    

    本质是在执行子类的构造方法时,第一行默认有一句代码 super(); ,如果显式给定该代码也必须放在构造方法的第一行

    如果需要调用父类的有参构造,需要显式写出:

    class Person {
        public Person(String name){							// 父类为有参构造
            System.out.println("执行父类构造方法");
        }
    }
    class Student extends Person{
        public Student(){
            super("victoria");								// 显式给定父类有参构造的执行
            System.out.println("执行子类构造方法");
        }
    }
    
  • 方法的重写

    • 需要有继承关系,子类重写父类的方法

    • 方法名、参数列表必须相同(当然,不要求形参变量名也完全相同)

    • 修饰符:范围可以扩大但不能缩小,修饰符范围:public > protected > default > private,比如父类是 protected,那么子类可以是 protected 或者 public

    • 抛出的异常:范围可以被缩小但不能扩大

    • 返回值类型:范围可以被缩小但不能扩大,即重写方法的返回值要么和父类方法一致,要么是其返回值的子类

    • 建议在重写的方法上边加上 @Override 注解,用于告诉计算机下面的方法是重写的,这样计算机也会校验重写语法是否正确

      @Override
      public void test() {}
      
    • 重写的本质:子类在继承父类的虚方法表时,如果发生了重写,就会进行覆盖

    • 只有能被添加到虚方法表中的方法才能被重写,例如静态方法和私有方法就不能被重写

    • 快捷键:Alt + Insert


多态

  • 一个对象的实际类型是确定的,但可以指向对象的引用的类型有很多

  • 父类的引用可以指向子类对象

    public class Application {
        public static void main(String[] args) {
            Student s1 = new Student();
            Person s2 = new Student();						// 父类的引用变量指向了子类对象
            Object s3 = new Student();						// 父类的父类的引用变量指向了子类对象
        }
    }
    class Person {}
    class Student extends Person {}
    
  • 多态的优劣:

    • 优势:多态形式下,右边对象可以实现解耦合,例如,定义方法时,使用父类作为参数,可以接收所有子类对象,提高了扩展性
    • 劣势:不能调用子类的特有功能
  • 多态调用成员的特点:

    • 调用成员变量:编译看左边,运行也看左边

      编译时看等号左边(父类)是否存在该成员变量,有则编译成功,否则编译失败

      运行时获取左边(父类)成员变量的值

    • 调用静态成员方法:编译看左边,运行看左边

      编译时看等号左边(父类)是否存在该成员方法,有则编译成功,否则编译失败

      运行时调用左边(子类)成员方法

    • 调用非静态成员方法:编译看左边,运行看右边

      编译时看等号左边(父类)是否存在该成员方法,有则编译成功,否则编译失败

      运行时调用右边(子类)成员方法

    public class Test {
        public static void main(String[] args) {
            Animal animal = new Dog();						// 多态
            System.out.println(animal.name);				// 输出为 "animal"
            animal.show();									// 输出为 "dog show"
        }
    }
    
    class Animal {
        String name = "animal";
        public void show() {
            System.out.println("animal show");
        }
    }
    
    class Dog extends Animal {
        String name = "dog";
        @Override
        public void show() {
            System.out.println("dog show");
        }
    }
    

Instanceof 和 类型转换

  • Instanceof :用于判断左侧引用类型变量指向的对象是否为右侧的类的对象、其子类的对象或是接口的实现类对象,如果左侧引用类型和右侧类型完全无关则会直接编译报错(编译通不通过取决于引用类型,返回值为true/false取决于具体指向的对象类型)

    public class Application {
        public static void main(String[] args) {
            // Object > String
            // Object > Person > Student
            // Object > Person > Teacher
            
            Object object = new Student();
            System.out.println(object instanceof Student);			// true
            System.out.println(object instanceof Person);			// true
            System.out.println(object instanceof Object);			// true
            System.out.println(object instanceof Teacher);			// false
            System.out.println(object instanceof String);			// false
    
            Person person = new Student();
            System.out.println(person instanceof Student);			// true
            System.out.println(person instanceof Person);			// true
            System.out.println(person instanceof Object);			// true
            System.out.println(person instanceof Teacher);			// false
            System.out.println(person instanceof String);			// 编译报错
    
            Student student = new Student();
            System.out.println(student instanceof Student);			// true
            System.out.println(student instanceof Person);			// true
            System.out.println(student instanceof Object);			// true
            System.out.println(student instanceof Teacher);			// 编译报错
            System.out.println(student instanceof String);			// 编译报错
        }
    }
    class Person { }
    class Student extends Person { }
    class Teacher extends Person { }
    
  • 对象的类型转换

    父类和子类的优先级:父类是高优先级,子类是低优先级,子类可以自动向父类转换,而父类转换成子类需要强制类型转换

    Person person = new Student();									// 多态
    Student student = (Student)person;								// 将Person类的引用变量转换成Student类的变量
    

    多态中错误的强制类型转换:

    public class Test {
        public static void main(String[] args) {
            Animal animal1 = new Animal();
            Animal animal2 = new Dog();
            Dog dog1 = (Dog)animal1;								// 报错,无法将Animal对象转换成Dog对象
            Dog dog2 = (Dog)animal2;								// 正确
            Cat cat1 = (Cat)animal1;								// 报错,无法将Animal对象转换成Cat对象
            Cat cat2 = (Cat)animal2;								// 报错,无法将Dog对象转换成Cat对象
        }
    }
    
    class Animal { }
    class Dog extends Animal { }
    class Cat extends Animal { }
    

static 关键字

  • 静态属性

    • 静态变量/类变量:归属于类,在内存中只有一个,被所有实例共享,可以通过 类名.属性名 或者 实例名.属性名 调用,推荐前者
    • 非静态变量/实例变量:归属于实例,不同实例相互独立,只能通过 实例名.属性名 调用
  • 静态方法

    • 静态方法/类方法:静态方法内只能调用静态变量/方法,静态方法中是没有 this 关键字的,可以通过 类名.方法名 或者 实例名.方法名 调用,推荐前者
    • 非静态方法/实例方法:非静态方法内可以调用静态变量/方法和非静态变量/方法,非静态方法中可以通过 this 关键字获取调用方法的对象的地址值,只能通过 实例名.方法名 调用
  • 静态代码块

    静态代码块随着类一同加载,最先执行,只执行一次

    public class Block {
        {
            System.out.print("匿名代码块 ");				// 匿名代码块(也称构造代码块)第二个执行
        }
        static {
            System.out.print("静态代码块 ");				// 静态代码块第一个执行
        }
        public Block() {
            System.out.print("构造方法 ");				// 构造方法第三个执行
        }
        public static void main(String[] args) {
            Block block1 = new Block();
            Block block2 = new Block();
        }
    }
    ===================================================================================
    输出结果:
    静态代码块 匿名代码块 构造方法 匿名代码块 构造方法
    
  • 静态导入包

    import 不加 static 修饰时只能导入类,而加了static 修饰后能导入类中的具体方法

    import static java.lang.Math.random;				// 导入Math类中的random方法
    

final 关键字

  • 使用 final 修饰方法:表明该方法是最终方法,不能被子类重写
  • 使用 final 修饰类:表明该类是最终类,不能被继承
  • 使用 final 修饰变量:叫做常量,只能被赋值一次
    • 如果是基本类型变量,那么变量存储的数据值不能发生改变

    • 如果是引用类型变量,那么变量存储的地址值不能发生改变,但对象内部的数据可以改变

    • 使用 final 修饰的变量一定要初始化,就算是类变量也必须初始化,并且只有三种初始化方式:1. 声明时赋值 2. 构造函数中赋值 3.构造代码块中赋值

      public class Test{
          final int a = 1;			// 正确,声明时赋值
          final int b;				// 正确,在构造函数中赋值
          final int c;				// 错误,无法通过其他方式赋值
          c = 3;
      
          public Test(int a) {
              this.b = 2;
          }
      }
      

权限修饰符

  • 用于控制一个成员能够被访问的范围

  • 可以修饰成员变量,方法,构造方法,部分内部类

  • 访问/权限修饰符:public、protected、private

    修饰符 同类 同包其他类 跨包子类 跨包其他类
    public
    protected ×
    default(不带修饰符时) × ×
    private × × ×
  • 使用 protected 修饰的非静态变量/方法的可见性:

    以下内容均为个人理解,可能存在偏差和谬误

    1. 对于使用 protected 修饰的非静态成员变量/方法,跨包子类能够进行访问,本质上是通过继承了父类中的成员实现的。也就是说,使用 protected 修饰的成员本质上还是同一包内可见的,这也是为什么跨包子类仍然无法访问父类实例的 protected 成员,但是跨包子类可以通过继承来的该成员来间接实现访问,也即可以访问自身实例中继承来的 protected 成员
    2. 对于继承来的 protected 修饰的成员,如果没有进行重写,那么该成员的可见域为父类所在包以及当前子类;如果进行了重写,那么该成员的可见域为当前子类所在包和其子类。这也是为什么,跨包子类可以通过创建自身实例来访问 protected 修饰的成员(可以访问自身的),但仍然无法通过创建父类实例来访问 protected 修饰的成员(无法访问父类的),也无法访问其他子类实例继承的 protected 修饰的成员(无法访问兄弟类的)
    3. 对于多级继承的情况,子类继承来的成员可见域包括三部分:①如果没有发生重写,那么顶级父类所在包为可见的;如果发生了重写,那么发生重写的中间父类所在包为可见的 ②上一步可见的包和当前子类中间的跨包父类也是可见的 ③当前子类也可见
    4. 总结:要判断子类中继承来的 protected 成员的可见域,需要先追根溯源找到该成员的定义位置,如果没有发生重写,那么将顶级父类作为主体,顶级父类所在的包内都是可见域,如果发生了重写,那么将最后一次重写的类作为新的主体,该类所在的包内都是可见域。找到了主体后,其下的中间父类和当前子类也是可见域
    // 包结构:
    // 		a包:A类
    // 		b包:B类,C类
    // 继承结构:
    // 		A类 -> B类,C类
    
    public class C extends A {
        public static void main(String[] args) {
            A father = new A();
            B son1 = new B();
            C son2 = new C();
            
            father.test();							// 报错,跨包子类无法通过创建父类实例来访问protected修饰的成员
            son1.test();							// 报错,跨包子类无法访问其他子类继承的protected修饰的成员
            son2.test();							// 正确用法
        }
    }
    

代码块

  • 局部代码块:方法内的代码块称为局部代码块,用于提前结束变量的生命周期

    public class Test {
        public static void main(String[] args) {
            // 局部代码块
            {
                int a = 10;
            }
        }
    }
    
  • 构造代码块/匿名代码块:和成员变量/方法同级的代码块称为构造代码块,该代码块会在构造方法执行前执行

    public class Test {
        // 构造代码块
        {
            System.out.println("可以抽取构造方法中的重复代码到构造代码块中");
        }
        public static void main(String[] args) {
        }
    }
    
  • 静态代码块:在构造代码块的基础上,加上了 static 关键字修饰,随着类的加载而加载,只会执行一次

    public class Test {
        // 静态代码块
        static {
            System.out.println("用于初始化");
        }
        public static void main(String[] args) {
        }
    }
    

    静态域执行顺序:静态属性/静态代码块 > 静态方法,可以存在多个静态代码块

  • 静态属性 和 静态代码块的执行顺序(非静态属性 和 非静态代码块同理)

    静态属性的内存分配是最先执行的,此时静态属性初始化为默认值,静态属性的显式初始化和静态代码块的内容按代码顺序执行

    例1:下面代码中,虽然静态属性的声明和初始化位于静态代码块之后,但并不会报错,x 先被静态代码块赋值为 5,再被初始化为10

    class Test {
        static {
            x = 5;
        }
        static int x = 10;
        public static void main(String[] args) {
            System.out.println(x);							// 输出为 10
        }
    }
    

    例2:下面代码中,虽然静态属性会先进行内存分配,但 Java 硬性规定了变量的使用必须在声明语句之后,上例中的赋值操作并不算使用,而本例中需要使用变量的值,因此此处编译直接就不通过了

    class Test {
        static {
            x += 5;											// 编译不通过
        }
        static int x = 10;
        public static void main(String[] args) {
            System.out.println(x);
        }
    }
    
  • 创建对象时代码执行顺序:

    静态属性分配内存 > 父类静态属性/父类静态代码块 > 子类静态属性/子类静态代码块 > 非静态属性分配内存 > 父类非静态属性/父类非静态代码块 > 父类构造方法 > 子类非静态属性/子类非静态代码块 > 子类构造方法

    注意事项一:静态属性/静态代码块的加载随着类加载进行,不会因为多次创建对象而多次执行

    注意事项二:对于父类中的成员变量,如果子类中没有同名的该变量,那么内存中只存在一个该变量,在子类中通过 this.变量名/super.变量名 或者在父类中通过 this.变量名 调用时都指向同一地址;如果子类中存在同名的该变量,那么内存中存在两个该变量,在子类通过 this.变量名 调用子类变量,通过 super.变量名 调用父类变量,再父类中通过 this.变量名 调用父类变量

    注意事项三:对于父类中的成员方法,如果在子类中重写了父类的方法,那么创建子类对象时,在父类非静态代码块/父类构造方法/父类非静态方法中执行的同名方法调用的都会是重写后的方法

    复杂例子(源自牛客网真题):分析程序的输出结果

    public class Test {
        public static void main(String[] args) {
            System.out.println(new B().getValue());
        }
    
        static class A {
            protected int value;
    
            public A(int v) {
                setValue(v);
            }
    
            public void setValue(int value) {
                this.value = value;
            }
    
            public int getValue() {
                try {
                    value++;
                    return value;
                } catch (Exception e) {
                    System.out.println(e.toString());
                } finally {
                    this.setValue(value);
                    System.out.println(value);
                }
                return value;
            }
        }
    
        static class B extends A {
            public B() {
                super(5);
                setValue(getValue() - 3);
            }
    
            public void setValue(int value) {
                super.setValue(2 * value);
            }
        }
    }
    

    答案为:22 34 17


抽象类

  • abstract 修饰符修饰的方法称为抽象方法,修饰的类称为抽象类
  • 抽象类中可以没有抽象方法,但是有抽象方法的类一定要声明为抽象类
  • 抽象类不能使用 new 关键字来创建对象,它是用来让子类继承的
  • 抽象方法只有方法的声明,没有方法的实现,它是用来让子类实现的
  • 子类继承抽象类,就必须要实现抽象类没有实现的抽象方法,否则该类也要声明为抽象类
public abstract class Action {							// Action为抽象类
    public abstract void doSomething();					// doSomething为抽象方法,注意没有大括号,以及要写分号
}

接口

  • 普通类 vs 抽象类 vs 接口:普通类只有具体实现,抽象类可以有具体实现和规范,接口只有规范

  • 声明类的关键字是 class,声明接口的关键字是 interface

  • 接口不能实例化,也没有构造方法

  • 接口中的所有属性默认都是使用 public static final 修饰的常量,所有方法默认都是使用 public abstract 修饰的

  • 接口的实现类对象可以访问到接口中的常量

  • 接口内的成员变量必须初始化,方法只有声明,方法具体实现通过 “实现类” 完成,实现类必须重写接口中的所有方法,否则为抽象类

    public interface UserService {							// 接口
        void run();											// 方法的声明
    }
    
    public class UserServiceImpl implements UserService{	// 实现类,使用implements关键字指明对应的接口
        @Override
        public void run() {									// 重写方法
            // 具体实现
        }
    }
    
  • 接口可以实现多继承,实现类还可以在继承一个类的同时实现多个接口

    public class UserServiceImpl extends Service implements UserService, TimeService{
        // 具体代码
    }
    
  • 什么时候需要用到接口?

    举个例子,目前有一个动物类作为父类,兔子类、狗类和鱼类作为子类,子类的行为可以分成三种,一种是所有子类共有的,例如吃,喝等行为,这类行为可以由父类抽取以起到节省代码和规范书写的作用;一种是某个子类独有的,这类行为可以单独书写在子类的成员方法中;还有一种是某些子类具备但另一些子类不具备的行为,例如游泳,这类行为无法抽取到父类中,写在子类中又会导致无法规范书写,这种时候就需要用到接口了。通过定义接口并在接口中声明方法,再让有需要的类实现该接口并重写方法,就可以起到规范作用

  • 接口和类之间的关系:

    • 类和类的关系:继承关系,只能单继承,不能多继承,但是可以多层继承
    • 接口和类的关系:实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口
    • 接口和接口的关系:继承关系,可以单继承,也可以多继承
    • 注意点:如果实现多个接口,则需要重写里面所有的抽象方法;如果实现的多个接口里有相同的抽象方法,只需要重写一次就行;如果实现子接口,则需要重写子接口和其父接口的抽象方法
  • 扩展内容:

    1. JDK8 开始接口中新增的方法:

      • JDK8 时接口中可以定义有方法体的方法,分成 默认方法静态方法

        • 默认方法

          作用:解决接口升级的兼容性问题(接口中方法增加了,只要使用默认方法就可以不用在每一个实现类中都新增重写内容了)

          格式:public default 返回值类型 方法名(参数列表) { }

          注意事项:默认方法不是抽象方法,不强制被重写,实现类在重写时需要去掉 default 关键字;public 可以省略,default 不能省略;如果实现了多个接口,且多个接口中存在相同名字的默认方法,那么实现类就必须对该方法进行重写

          public interface Inter {
              public default void run() {					// 使用default修饰,表明是默认方法而非抽象方法
          		// 方法体
              }
          }
          
        • 静态方法

          格式:public static 返回值类型 方法名(参数列表) { }

          注意事项:静态方法只能通过接口名调用,不能被重写(不会被视为重写);public 可以省略,static 不能省略

      • JDK9 时接口中可以定义私有方法

        • 普通私有方法

          作用:用于给接口中的默认方法调用,并且不对外公开

          格式:private 返回值类型 方法名(参数列表)

        • 静态私有方法

          作用:用于给接口中的静态方法调用,并且不对外公开

          格式:private static 返回值类型 方法名(参数列表)

    2. 接口的应用

      • 接口代表规则,是行为的抽象,想要让哪个类拥有一个行为,就让这个类实现对应的接口就可以了
      • 当一个方法的参数是接口时,可以传递接口所有实现类的对象,这种方式称为接口的多态(同样遵循编译看左,运行看右)
    3. 适配器设计模式

      • 当一个接口中抽象方法过多,但实现类中只要使用其中一部分的时候,就可以使用适配器设计模式
      • 书写步骤:1.编写中间类 XXXAdapter,实现对应的接口,对接口中的抽象方法进行空实现,2.然后让真正的实现类继承中间类,并重写需要用的方法,这样就不用在实现类中重写不需要的方法了,3.为了避免其他类创建中间类的对象,中间类用abstract 进行修饰

内部类

  • 内部类就是在一个类的内部再定义一个类

  • 例如,A类中定义了B类,那么B类相对于A类来说就称为内部类,而A类相对于B类就称为外部类

  • 内部类可以直接访问外部类的成员,包括私有的;外部类要访问内部类的成员,必须创建对象

  • 内部类的分类:成员内部类、静态内部类、局部内部类、匿名内部类

    1. 成员内部类

      public class Application {
          public static void main(String[] args) {
              // 成员内部类获取方法一:直接创建
              Outer.Inner inner1 = new Outer().new Inner();
              // 成员内部类获取方法二:外部类提供获取方法
              Outer outer = new Outer();
              Outer.Inner inner2 = outer.getInstance();
          }
      }
      class Outer {
          private int id;
          public class Inner {									// 成员内部类
      		public void getID(){
                  System.out.println(id);							// 优势:内部类可以获得外部类的私有属性/方法
              }
          }
          public Inner getInstance() {
              return new Inner();
          }
      }
      

      应用题:

      public class Test {
          public static void main(String[] args) {
              new Outer().new Inner().show();
          }
      }
      class Outer {
          private int a = 10;
          public class Inner {
              private int a = 20;
              public void show() {
                  int a = 30;
                  System.out.println(Outer.this.a);				// 输出为 10,Outer.this是外部类对象地址值
                  System.out.println(this.a);						// 输出为 20,this是当前对象地址值
                  System.out.println(a);							// 输出为 30,就近原则
              }
          }
      }
      
    2. 静态内部类

      使用 static 对成员内部类进行修饰,与成员内部类的区别主要在于静态内部类中只能调用静态属性/方法

      public class Test {
          public static void main(String[] args) {
              Outer.Inner inner = new Outer.Inner();				// 静态内部类的创建
              inner.show();
          }
      }
      class Outer {
          public static class Inner {
              public void show() {
              }
          }
      }
      
    3. 局部内部类

      写在方法内的类称为局部内部类

      public class Test {
          public static void main(String[] args) {
          }
      }
      class Outer {
          public void show() {
              class Inner {
              }
              Inner inner = new Inner();
          }
      }
      
    4. 匿名内部类

      定义:隐藏了名字的内部类,可以写在成员位置,也可以写在局部位置

      格式:new 类名或者接口名() { 重写方法 }; 要注意大括号后的分号不要忘了!

      格式分析:实际上,由大括号包围的内容才是匿名内部类,其余部分是创建了一个匿名内部类的对象的意思

      • 匿名内部类 实现 接口

        public class Test {
            public static void main(String[] args) {
                // 匿名内部类 --> 接口实现类的对象
                new Swim() {										// 1.匿名内部类实现Swim接口,3.new创建对象
                    @Override
                    public void swim() {							// 2.重写接口中的抽象方法
                        System.out.println("重写方法");
                    }
                };
            }
        }
        
        interface Swim {
            public abstract void swim();
        }
        
      • 匿名内部类 继承 类

        public class Test {
            public static void main(String[] args) {
                // 匿名内部类 --> 类的子类对象
                new Animal() {										// 1.匿名内部类继承Animal类,3.new创建对象
                    @Override
                    public void eat() {								// 2.重写类中的抽象方法
                        System.out.println("重写方法");
                    }
                };
            }
        }
        
        abstract class Animal {
            public abstract void eat();
        }
        
      • 应用场景

        当方法的参数是接口或者抽象类时,如果实现类/子类只要使用一次,就可以用匿名内部类简化代码

        举个例子:

        public class Test {
            public static void main(String[] args) {
                // 目前需要编写代码调用method方法,由于Animal是抽象类,因此在传统方法中需要先为Animal类定义一个子类
                // 之后创建子类对象,再将子类对象作为method方法的参数,但有了匿名内部类后可以大大简化这一实现
                // 本质是使用匿名内部类继承了抽象类,然后使用new创建了一个匿名内部类的对象,再将该对象作为实参传递给method
                method(
                        new Animal() {
                            public void eat() {
                                System.out.println("重写方法");
                            }
                        }											// 注意这里不需要写分号了
                );
            }
            public static void method(Animal a) {
                a.eat();
            }
        }
        
        abstract class Animal {
            public abstract void eat();
        }
        

Lambda 表达式

  • Lambda 表达式是 JDK8 新增的一种语法格式

  • 格式:()->{},小括号内为方法的形参,箭头为固定格式,大括号内为方法体

  • 作用:简化函数式接口的匿名内部类的写法

  • 注意事项:

    1. Lambda 表达式可以用来简化匿名内部类的书写,但只能用于简化函数式接口的匿名内部类的写法
    2. 函数式接口:有且仅有一个抽象方法的接口叫做函数式接口(首先必须是接口,其次只能有一个抽象方法),接口上方可以加 @FunctionalInterface 注解
  • 示例:使用 Lambda 表达式简化匿名内部类的书写

    图6-2

  • Lambda 表达式的省略写法:

    • 参数类型可以省略不写

    • 如果只有一个参数,参数类型可以省略,同时()也可以省略

    • 如果 Lambda 表达式的方法体只有一行,那么大括号、分号、return可以省略不写,但需要同时省略

    • 示例:

      // Lambda表达式完整写法
      Arrays.sort(arr, (Integer o1, Integer o2) -> {
              return o1 - o2;
          }
      );
      // Lambda表达式省略写法
      Arrays.sort(arr, (o1, o2) -> o1 - o2);
      
posted @   victoria6013  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示