java学习笔记
tbghg

[TOC]

记录一下学习java时觉得重点或一时没记住的知识点,方便日后复习,持续更新,有东西就加

java基础

认识java

Java介于编译型语言和解释型语言之间。编译型语言如C、C++,代码是直接编译成机器码执行,但是不同的平台(x86、ARM等)CPU的指令集不同,因此,需要编译出每一种平台的对应机器码。

解释型语言如Python、Ruby没有这个问题,可以由解释器直接加载源码然后运行,代价是运行效率太低。

而Java是将代码编译成一种“字节码”,它类似于抽象的CPU指令,然后,针对不同平台编写虚拟机,不同平台的虚拟机负责加载字节码并执行,这样就实现了“一次编写,到处运行”的效果。当然,这是针对Java开发者而言。

对于虚拟机,需要为每个平台分别开发。为了保证不同平台、不同公司开发的虚拟机都能正确执行Java字节码,SUN公司制定了一系列的Java虚拟机规范。从实践的角度看,JVM的兼容性做得非常好,低版本的Java字节码完全可以正常运行在高版本的JVM上。

  • Java SE(Standard Edition): 标准版,包含标准的JVM和标准库
  • Java EE(Enterprise Edition): 企业版,在Java SE的基础上加上了大量的API和库,以便方便开发Web应用、数据库、消息服务等
  • Java ME(Micro Edition): 针对嵌入式设备的“瘦身版”,Java SE的标准库无法在Java ME上使用,Java ME的虚拟机也是“瘦身版”
┌───────────────────────────┐
│Java EE                    │
│    ┌────────────────────┐ │
│    │Java SE             │ │
│    │    ┌─────────────┐ │ │
│    │    │   Java ME   │ │ │
│    │    └─────────────┘ │ │
│    └────────────────────┘ │
└───────────────────────────┘
  • JRE(Java Runtime Environment): 运行Java字节码的虚拟机
  • JDK(Java Development Kit)

如果只有Java源码,要编译成Java字节码,就需要JDK,JDK除了包含JRE,还提供了编译器、调试器等开发工具。

┌─    ┌──────────────────────────────────┐
│     │     Compiler, debugger, etc.     │
│     └──────────────────────────────────┘
JDK ┌─ ┌──────────────────────────────────┐
│   │  │                                  │
│  JRE │      JVM + Runtime Library       │
│   │  │                                  │
└─  └─ └──────────────────────────────────┘
┌───────┐┌───────┐┌───────┐┌───────┐
│Windows││ Linux ││ macOS ││others │
└───────┘└───────┘└───────┘└───────┘
  • JSR规范:Java Specification Request
  • JCP组织:Java Community Process

为了保证Java语言的规范性,SUN公司搞了一个JSR规范,凡是想给Java平台加一个功能,比如说访问数据库的功能,大家要先创建一个JSR规范,定义好接口,这样,各个数据库厂商都按照规范写出Java驱动程序,开发者就不用担心自己写的数据库代码在MySQL上能跑,却不能跑在PostgreSQL上。

所以JSR是一系列的规范,从JVM的内存模型到Web程序接口,全部都标准化了。而负责审核JSR的组织就是JCP。

一个JSR规范发布时,为了让大家有个参考,还要同时发布一个“参考实现”,以及一个“兼容性测试套件”:

  • RI:Reference Implementation
  • TCK:Technology Compatibility Kit

比如有人提议要搞一个基于Java开发的消息服务器,这个提议很好啊,但是光有提议还不行,得贴出真正能跑的代码,这就是RI。
如果有其他人也想开发这样一个消息服务器,如何保证这些消息服务器对开发者来说接口、功能都是相同的?所以还得提供TCK。

通常来说,RI只是一个“能跑”的正确的代码,它不追求速度,所以,如果真正要选择一个Java的消息服务器,一般是没人用RI的,大家都会选择一个有竞争力的商用或开源产品。

运行一个Demo,文件名为Hello.java,代码如下

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

javac命令将Hello.java文件编译成字节码文件Hello.class,然后用java命令执行这个字节码文件

D:\Desktop\Java_project>javac Hello.java

D:\Desktop\Java_project>dir
2023/11/30  16:05    <DIR>          .
2023/11/29  22:19    <DIR>          ..
2023/11/30  16:05               417 Hello.class
2023/11/30  16:04               122 Hello.java

D:\Desktop\Java_project>java Hello
Hello, world!

注意:给虚拟机传递的参数Hello是我们定义的类名,虚拟机自动查找对应的class文件并执行,

Java 11之后,可以通过java Hello.java可以直接运行一个单文件源码,需要注意的是,在实际项目中,单个不依赖第三方库的Java源码是非常罕见的。
所以,绝大多数情况下,我们无法直接运行一个Java源码文件,原因是它需要依赖其他的库。

  • 一个Java源码只能定义一个public类型的class,并且class名称和文件名要完全一致;
  • 使用javac可以将.java源码编译成.class字节码;
  • 使用java可以运行一个已编译的Java程序,参数是类名。

基础语法

Java是面向对象的语言,一个程序的基本单位就是class,类名要求:必须以英文字母开头(大写),后接字母,数字和下划线的组合

Java入口程序规定的方法必须是静态方法,方法名必须为main,括号内的参数必须是String数组。

方法名也有命名规则,命名和class一样,但是首字母小写,在方法内部,语句才是真正的执行代码。Java的每一行语句必须以分号结束。

public class Hello {
    public static void main(String[] args) { // 方法名是main
        System.out.println("Hello, world!"); // 语句分号结束
    }
}

注释有三种:

// 单行注释

/*
多行注释
这是个注释
还是个注释
*/

/**
 * 可以用来自动创建文档的注释
 * 
 * @auther tbghg
 */

第三种以/**开头,以*/结束,如果有多行,每行通常以星号开头,需要写在类和方法的定义处,可以用于自动创建文档。

数据类型

基本类型

基本类型如下:

  • 整数类型:byte,short,int,long
  • 浮点数类型:float,double
  • 字符类型:char
  • 布尔类型:boolean

计算机内存的最小存储单元是字节(byte),一个字节就是一个8位二进制数,即8个bit,不同数据类型所占字节数:

  • byte 1字节 -128 ~ 127
  • short 2字节 -32768 ~ 32767
  • char 2字节
  • int 4字节 -2147483648 ~ 2147483647
  • float 4字节
  • long 8字节 -9223372036854775808 ~ 9223372036854775807
  • double 8字节
public class Main {
    public static void main(String[] args) {
        int i = 2147483647;
        int i2 = -2147483648;
        int i3 = 2_000_000_000; // 加下划线更容易识别
        int i4 = 0xff0000; // 十六进制表示的16711680
        int i5 = 0b1000000000; // 二进制表示的512

        long n1 = 9000000000000000000L; // long型的结尾需要加L
        long n2 = 900; // 没有加L,此处900为int,但int类型可以赋值给long
        int i6 = 900L; // 错误:不能把long型赋值给int
        
        
        float f1 = 3.14f;
        float f2 = 3.14e38f; // 科学计数法表示的3.14x10^38
        float f3 = 1.0; // 错误:不带f结尾的是double类型,不能赋值给float
        
        double d = 1.79e308;
        double d2 = -1.79e308;
        double d3 = 4.9e-324; // 科学计数法表示的4.9x10^-324
        
        // Java语言对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要1 bit,但是通常JVM内部会把boolean表示为4字节整数。
        boolean b1 = true;
        boolean b2 = false;
        boolean isGreater = 5 > 3; // 计算结果为true
        int age = 12;
        boolean isAdult = age >= 18; // 计算结果为false
        
        // 字符类型char表示一个字符。字符使用单引号。一个char保存一个Unicode字符
        char a = 'A';    // Unicode编码,都占用两字节
        char zh = '中';  // Unicode编码,都占用两字节
        
        System.out.println(a);
        System.out.println(zh);
        
        // 可以直接用转义字符\u+Unicode编码来表示一个字符,必须使用十六进制
        char c3 = '\u0041'; // 'A',因为十六进制0041 = 十进制65
        char c4 = '\u4e2d'; // '中',因为十六进制4e2d = 十进制20013
        
        System.out.println(c3);
        System.out.println(c4);

    }
}

整数运算:

  • 整数的除法对于除数为0时运行时将报错,但编译不会报错
  • 整数除法不能整除会舍弃小数
  • 溢出不会出错,却会得到一个奇怪的结果
  • i++ ++i << >> >>>无符号右移 & | ~非 ^异或
  • 类型不统一时,会自动转换:在运算过程中,如果参与运算的两个数类型不一致,那么计算结果为较大类型的整型
  • 支持强制转换,如short s = (short) i

浮点数运算:

  • 只能进行加减乘除这些数值计算,不能做位运算和移位运算
  • 浮点数常常无法精确表示,如0.1,二进制是无限循环小数,但0.5又可以精确地表示,因此,浮点数比较大小有时是不准确的(见下方代码)
  • 其他与整形运算相似
public class Main {
    public static void main(String[] args) {
        // 0.1在二进制是无限循环小数,只能存储一个近似值,所以x与y不相等
        double x = 1.0 / 10;
        double y = 1 - 9.0 / 10;
        // 观察x和y是否相等:
        System.out.println(x);
        System.out.println(y);
        System.out.println(x == y);

        // 0.5在二进制中可精确表示,所以x与y相等
        x = 5.0 / 10;
        y = 1 - 5.0 / 10;
        System.out.println(x);
        System.out.println(y);
        System.out.println(x == y);
    }
}

/* output:
0.1
0.09999999999999998
false
0.5
0.5
true
*/

如果非要比较,可以设置一个阈值,看二者相差是否在这之间:

// 比较x和y是否相等,先计算其差的绝对值:
double r = Math.abs(x - y);
// 再判断绝对值是否足够小:
if (r < 0.00001) {
    // 可以认为相等
} else {
    // 不相等
}

布尔运算:

  • 短路运算:如果一个布尔运算的表达式能提前确定结果,则后续的计算不再执行,直接返回结果
  • 支持三元运算符:b ? x : y
引用类型

除了基本类型,剩下的都是引用类型,最常用的是String,引用类型的变量类似于C语言的指针,它内部存储一个“地址”,指向某个对象在内存的位置

String,用双引号”…”表示字符串。一个字符串可以存储0个到任意个字符,转义:

  • " 表示字符”
  • ' 表示字符’
  • \ 表示字符\
  • \n 表示换行符
  • \r 表示回车符
  • \t 表示Tab
  • \u#### 表示一个Unicode编码的字符

字符串拼接可直接使用+。如果用+连接字符串和其他数据类型,会将其他数据类型先自动转型为字符串,再连接。

从Java 13开始,字符串可以用”””…”””表示多行字符串(Text Blocks)了:

public class Main {
    public static void main(String[] args) {
        // 末尾会带上换行,总共五行
        String s = """
                   SELECT * FROM
                     users
                   WHERE id > 100
                   ORDER BY name DESC
                   """;
           
        // 末尾没换行,共四行
        String s2 = """
                   SELECT * FROM
                     users
                   WHERE id > 100
                   ORDER BY name DESC""";
        System.out.println(s);
    }
}

引用类型的变量可以指向一个空值null,它表示不存在,即该变量不指向任何对象。例如:

String s1 = null; // s1是null
String s2 = s1; // s2也是null
String s3 = ""; // s3指向空字符串,不是null

常量:定义变量的时候,如果加上final修饰符,这个变量就变成了常量,常量在定义时进行初始化后就不可再次赋值,再次赋值会导致编译错误。根据习惯,常量名通常全部大写。

final double PI = 3.14; // PI是一个常量
double r = 5.0;
double area = PI * r * r;
PI = 300; // compile error!

有些时候,类型的名字太长,写起来比较麻烦。例如:

StringBuilder sb = new StringBuilder();

这个时候,如果想省略变量类型,可以使用var关键字,编译器会根据赋值语句自动推断出变量sb的类型是StringBuilder

var sb = new StringBuilder();

多行语句用{ }括起来,括号内部是自身的范围,在语句块中定义的变量,它有一个作用域,就是从定义处开始,到语句块结束。超出了作用域引用这些变量,编译器会报错。

public class Main {
    public static void main(String[] args) {
        {
            int i = 0; // 变量i从这里开始定义
            {
                int x = 1; // 变量x从这里开始定义
                {
                    String s = "hello"; // 变量s从这里开始定义
                } // 变量s作用域到此结束
                
//                System.out.println(s);  // 报错,无法解析符号 's'
                
                // 注意,这是一个新的变量s,它和上面的变量同名,
                // 但是因为作用域不同,它们是两个不同的变量:
                String s = "hi";
                
            } // 变量x和s作用域到此结束
        } // 变量i作用域到此结束
    }
}

数组:

public class Main {
    public static void main(String[] args) {
        // 5位同学的成绩:
        int[] ns = new int[5];
        System.out.println(ns.length); // 5
        
        // 也可以在定义数组时直接指定初始化的元素,这样就不必写出数组大小,而是由编译器自动推算数组大小
        int[] ns = new int[] { 68, 79, 91, 85, 62 };
        
        // 可以进一步简写为
        int[] ns = { 68, 79, 91, 85, 62 };

    }
}
  • 数组所有元素初始化为默认值,整型都是0,浮点型是0.0,布尔型是false
  • 数组一旦创建后,大小就不可改变

流程控制

// todo 输入输出
输入输出:这块之后系统整理下

if判断语句:

if (条件) {
    // 条件满足时执行
} else if (条件) {

} else {

}
  • 引用类型判断内容相等要使用equals(),注意避免NullPointerException【重点】
  • 浮点数判断相等不能直接用==运算符
  • 不推荐省略花括号{} 一条语句时大括号可以省,但不推荐
public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "HELLO".toLowerCase();
        // 引用类型 使用equals判断相等
        if (s1.equals(s2)) {
            System.out.println("s1 equals s2");
        }
        
        String s3 = null;
        // 必须先判断null,否则会报NullPointerException错误
        if (s3 != null && s3.equals("hello")) {
            System.out.println("hello");
        }
    }
}

switch-case语句

  • switch的计算结果必须是整型、字符串或枚举类型
  • case语句具有穿透性,要带break,可以打开fall-through警告
  • 不要漏掉default,可以打开missing default警告
  • Java 14开始,switch语句正式升级为表达式,使用->格式时,不再需要break,允许携带返回值,通过yield提前中断(类似于return)
public class Main {
    public static void main(String[] args) {
        String fruit = "orange";
        int opt = switch (fruit) {
            // 直接返回1
            case "apple" -> 1;
            // 直接返回2
            case "pear", "mango" -> 2;
            // 整个代码块用{}包裹,yield可提前结束switch-case语句,并返回结果
            default -> {
                int code = fruit.hashCode();
                yield code; // switch语句返回值
            }
        };
        System.out.println("opt = " + opt);
    }
}

循环语句

while (条件表达式) {
    循环语句
}

do {
    执行循环语句
} while (条件表达式);

for (初始条件; 循环检测条件; 循环后更新计数器) {
    // 执行语句
}

// 遍历数组时可使用for-each,n为ns中的元素
for (int n : ns) {
    // 执行语句
}

// 当然,也是支持break、continue的

数组操作

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] ns = { 1, 1, 8, 3, 5, 4 };
        
        // 1. 遍历
        
        // 直接打印,打印出来的是JVM中的地址,如
        System.out.println(ns);
        
        for (int n : ns) {
            System.out.print(n+" ");
        }
        System.out.println();
        
        // java提供了Arrays.toString方法便于打印
        System.out.println(Arrays.toString(ns));
        
        /* output:
        [I@b4c966a
        1 1 8 3 5 4 
        [1, 1, 8, 3, 5, 4]
        */
        
        // 2. 排序
        
        Arrays.sort(ns);
        if (Arrays.toString(ns).equals("[1, 1, 3, 4, 5, 8]")) {
            System.out.println("排序正确");
        }
        
        // 3. 二维数组
        
        int[][] ns2 = {
                { 1, 2, 3, 4 },
                { 5, 6, 7, 8 },
                { 9, 10, 11, 12 }
        };

        // 可以两层for遍历
        for (int[] arr : ns2) {
            for (int n : arr) {
                System.out.print(n);
                System.out.print(" ");
            }
            System.out.println();
        }

        // 可以Arrays.deepToString()打印
        System.out.println(Arrays.deepToString(ns2));
        
        /* output:
        1 2 3 4 
        5 6 7 8 
        9 10 11 12 
        [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
        */
    }
}

面向对象

Java是一种面向对象的编程语言。面向对象编程,英文是Object-Oriented Programming,简称OOP

  • 一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中
  • class和instance是“模版”和“实例”的关系,定义class就是定义了一种数据类型,对应的instance是这种数据类型的实例
  • class定义的field,在每个instance都会拥有各自的field,且互不干扰
  • 通过new操作符创建新的instance,然后用变量指向它,即可通过变量来引用这个instance
  • 指向instance的变量都是引用变量

方法

修饰符 方法返回类型 方法名(方法参数列表) {
    若干方法语句;
    return 方法返回值;
}
  • 方法内部可以使用this访问当前实例
  • 方法支持可变参数,如public void setNames(String... names) {}
    • 可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null
    • setNames此时的参数names是String[]类型

构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Xiao Ming", 15); // 既可以调用带参数的构造方法
        Person p2 = new Person(); // 也可以调用无参数构造方法
    }
}

class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
  • 实例在创建时通过new操作符会调用其对应的构造方法,构造方法用于初始化实例
  • 没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法
  • 可以定义多个构造方法,编译器根据参数自动判断
  • 可以在一个构造方法内部调用另一个构造方法,便于代码复用

方法重载(overload):如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。

注意:方法重载的返回值类型通常都是相同的。

方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。举个例子,String类提供了多个重载方法indexOf(),可以查找子串:

  • int indexOf(int ch):根据字符的Unicode码查找
  • int indexOf(String str):根据字符串查找
  • int indexOf(int ch, int fromIndex):根据字符查找,但指定起始位置
  • int indexOf(String str, int fromIndex)根据字符串查找,但指定起始位置

继承
  • 继承是面向对象编程的一种强大的代码复用方式
  • Java只允许单继承,所有类最终的根类是Object
  • protected允许子类访问父类的字段和方法
  • 子类的构造方法可以通过super()调用父类的构造方法
  • 可以安全地向上转型为更抽象的类型
  • 可以强制向下转型,最好借助instanceof判断
  • Java 14后instanceof判断完可直接赋值
  • 子类和父类的关系是is,has关系不能用继承,可考虑组合

使用extends关键字来实现继承。超类(super class),父类(parent class),基类(base class) | 子类(subclass),扩展类(extended class)

注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!

子类无法访问父类的private字段或者private方法,为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问

使用super可以指向父类(对应this),子类的构造方法必须调用父类的构造方法,方式为super()并携带上对应参数

正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。

  • 向上转型:把一个子类型安全地变为更加抽象的父类型【没问题】
  • 向下转型:把一个父类类型强制转型为子类类型【看原本是什么,如果原本就能转那没问题,否则会报错ClassCastException

向下转型举例:

Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。

因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException。

为了防止向下转型发生问题,一般先通过instanceof方法判断是否为该实例

Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false

Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true

Student n = null;
System.out.println(n instanceof Student); // false

从Java 14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:

Object obj = "hello";
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.toUpperCase());
}

可以改写如下:

public class Main {
    public static void main(String[] args) {
        Object obj = "hello";
        // instanceof判断完成后转换并赋到s中
        if (obj instanceof String s) {
            // 可以直接使用变量s:
            System.out.println(s.toUpperCase());
        }
    }
}
多态
  • 子类可以覆写父类的方法(Override),覆写在子类中改变了父类方法的行为
  • Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态
  • final修饰符有多种作用
    • final修饰的方法可以阻止被覆写
    • final修饰的class可以阻止被继承
    • final修饰的field必须在创建对象时初始化,随后不可修改

重载overload方法签名不同,覆写(重写)override方法签名不同相同

加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。

多态的特性就是,运行期才能动态决定调用的子类方法。多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。

public class Main {
    public static void main(String[] args) {
        Person p = new Student();
        p.run(); // 应该打印Person.run还是Student.run?

        p.runTwice(p);
    }
}

class Person {
    public void run() {
        System.out.println("Person.run");
    }
    public void runTwice(Person p) {
        // run方法有可能会被重写,我们并不知道执行时是person的还是重写后student的,只有根据运行时期实际类型才知道
        p.run();
        p.run();
    }
    // 被final修饰,无法重写
    public final String hello() {
        return "Hello, " + name;
    }
}

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

// final修饰的类无法被继承
final class Apple {
    // 可直接赋值,也可在构造方法中赋值,之后不能再修改该字段
    public final String name = "Unamed";
}
抽象类

如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:

class Person {
    public abstract void run();
}

把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。

必须把Person类本身也声明为abstract,才能正确编译它:

abstract class Person {
    public String name;
    public abstract void run();
}

我们无法实例化一个抽象类:Person p = new Person(); // 编译错误。抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。

当我们定义了抽象类Person,以及具体的Student、Teacher子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例,这种引用抽象类的好处在于,我们对其进行方法调用,并不关心Person类型变量的具体子类型:

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

// 不关心Person变量的具体子类型:
s.run();
t.run();

这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。

面向抽象编程的本质就是:

  • 上层代码只定义规范(例如:abstract class Person)
  • 不需要子类就可以实现业务逻辑(正常编译)
  • 具体的业务逻辑由不同的子类实现,调用者并不关心
接口

如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口:interface

所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。

interface Person {
    void run();
    String getName();
}

当一个具体的class去实现一个interface时,需要使用implements关键字。举个例子:

class Student implements Person {
    private String name;

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

    @Override
    public void run() {
        System.out.println(this.name + " run");
    }

    @Override
    public String getName() {
        return this.name;
    }
}

在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interfaceclass Student implements Person, Hello {...}

abstract class interface
继承 只能extends一个class 可以implements多个interface
字段 可以定义实例字段 不能定义实例字段,可定义静态字段且为final类型
抽象方法 可以定义抽象方法 可以定义抽象方法
非抽象方法 可以定义非抽象方法 可以定义default方法

在接口中,可以定义default方法。实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。

default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

public class Main {
    public static void main(String[] args) {
        Person p = new Student("Xiao Ming");
        p.run();
    }
}

interface Person {
    String getName();
    default void run() {
        System.out.println(getName() + " run");
    }
}

class Student implements Person {
    private String name;

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

    public String getName() {
        return this.name;
    }
}
 评论