Java21 编程基础

927 阅读49分钟

Java21 基础

image.png

基本介绍

Java 语言的主要创造者是 James Gosling。 他是一位在业界广受尊重的计算机科学家,被誉为 "Java 之父"。他在 Sun Microsystems 参与 "绿色项目",在该项目中他领导了 Java 语言的创造和开发。这使得 Java1995 年对外发布,并迅速成为了世界上最流行的编程语言之一。

image.png

里程碑

Java 的历史回溯到 1991 年,Sun Microsystems 的 "绿色项目" 小组开始了 Java 语言的开发。他们的目标是为数字设备(如电视,电话等)编写程序。Java 1.01995 年公开发布,给出的理念就是 "Write Once, Run Anywhere"。1998 年,Java 2 Standard Edition (J2SE) 发布,提供了独立运行 Java 应用程序所需的运行时环境。同一年,Java 2 Enterprise Edition (J2EE) 暨现在的 Jakarta EE 也发布了,这为构建企业级应用提供了强大的工具。

自那时以来,Java 经历了几个重要的里程碑:

  1. 2004年 - Java 5.0:发布了大量的新特性,包括泛型、自动装箱/拆箱、增强的 for 循环,以及枚举等。
  2. 2006年Sun 发布了 Java 的开源版本 - OpenJDK
  3. 2009年 - Oracle 收购SunOracle 收购了 Sun,从而成为了 Java 的持有者。
  4. 2014年 - Java 8:引入了函数式编程的概念,包括 Lambda 表达式和流操作等。
  5. 2018年 - Java 11:是长期支持版本 (LTS)Oracle 改变了商业许可政策。

现状分析

下面是 20241TIOBE 指数:

2024-01-tiobe

首先,我们观察到,一些新兴的编程语言,比如 PythonJavaScriptGo,因为适时应对了技术发展和市场需求的变化,因此它们的流行度正在快速上升。这要归功于它们的易用性、灵活性以及在特定领域如数据科学、Web 开发、并发和系统级编程等方面的优势。

另外,Oracle 公司在 Java 11 和之后的版本中对于其 JDK 的商业许可政策进行了更改,这也影响了一部分商业用户对 Java 的看法和使用情况。这导致一些公司和开发者开始寻找其他的免费或者更加符合其特定需求的编程环境。

此外,C# 的开源以及 .Net 发展的稳定性及其生态的成熟,比如跨平台的 .NET Core 的流行,也受到了开发者们的欢迎,也是 Java 排名下滑的一个因素。

然而,尽管面临这些挑战,Java 作为一门有着丰富历史和广泛应用的编程语言,它在很多领域还是无可替代的。例如在企业级应用、Android 应用开发以及 Hadoop 大数据处理方面,Java 依然是主导地位。

特别值得一提的是 Spring 框架以及其衍生的 Spring BootSpring Cloud,它们极大地推进了 Java 在企业级开发以及微服务架构方面的应用。Spring Boot 通过提供快速的项目启动方式以及各种开箱即用的 Starter,极大地提升了 Java 的开发效率。而 Spring Cloud 则提供了一整套的微服务解决方案,使得 Java 在微服务领域的应用更加方便和高效。

总之,尽管 Java 在排行榜上的位置有所下降,但由于其强大的性能、稳定的系统以及丰富的生态系统,Java 仍然是一门极其重要且有活力的编程语言。

环境组成

以下表示 JDKJREJVM 包含关系:

+----------------------------------------------+
|        JDK                                   |
| +-----------------------+ +----------------+ |
| |      JRE              | |  Other Tools   | |
| | +-----------+         | |                | |
| | |    JVM    |         | |  javac,        | |
| | +-----------+         | |  java,         | |
| |                       | |  javadoc,      | |
| |  lib (rt.jar, etc.)   | |  jdb           | |
| +-----------------------+ +----------------+ |
+----------------------------------------------+
  • JDK(Java Development Kit):是 Java 开发者工具包,它包含了 JREJava 运行环境)和其他开发工具(如 javac 编译器,javadocjdb 调试器等)。
  • JRE(Java Runtime Environment):是 Java 运行环境,它是 JDK 的一部分,负责提供 Java 程序运行所需的环境。它包含了JVMJava 虚拟机)和核心类库。Java 类库提供了大量预编译的类,包括基础的数据结构和算法,网络编程,文件操作,图形用户界面等,以方便开发者使用。
  • JVM(Java Virtual Machine):是 Java 虚拟机,它负责字节码的执行。JVM 是平台相关的,能让相同的 Java 字节码在不同的操作系统和硬件平台上运行。

重要特征

  • Java 作为面向对象编程(OOP) 模型的佼佼者,深受全球开发者的青睐。
  • Java 凭借其强类型特性和自带的垃圾回收(GC) 机制,保证了代码的安全性和健壮性。
  • Java 融合了编译性和解释性的优点,提供了灵活且高效的代码执行方式。
  • Java 具有跨平台性,体现了 "Write Once, Run Anywhere" 的理念。

运行机制

Java 编译执行命令:

# 将Java源代码进行编译
➜ javac Hello.java

# 查看生成的字节码文件
➜ ll Hello.class
-rw-r--r--@ 1 mystic  staff  1117  1  6 16:13 Hello.class

# 启动Java应用程序
➜ java Hello
Hello World

Java 基本执行流程:

+----------------------------+
|         Source Code        | --- MyClass.java 源代码文件
+----------------------------+
              |
              ▼
+----------------------------+
|        Java Compiler       | --- 命令:javac(由源代码编译至字节码的过程,被称为 “编译阶段”)
+----------------------------+
              |
              ▼
+----------------------------+
|   Byte Code (.class file)  | --- MyClass.class 字节码二进制文件
+----------------------------+
              |
              ▼
+----------------------------+
| JVM (Java Virtual Machine) | --- 命令:java(不同平台的 JVM 会把字节码转换成能在相应平台运行的机器码,被称为 “解释阶段”)
+----------------------------+
              |
              ▼
+----------------------------+
|     Running Application    | --- MyClass 应用程序
+----------------------------+

Java 同时具有编译性和解释性,这使其既能享受到编译型语言的运行效率,又能享受到解释型语言的跨平台特性。

阶段过程语言特性软件/命令输入输出输出类型
开发阶段编译编译性javac 编译器Java源代码 (*.java 文件)字节码 (*.class 文件)二进制字节码
运行阶段解释执行解释性Java虚拟机 (JVM)字节码 (*.class 文件)直接执行无(在 JVM 中执行)
运行阶段即时编译 (JIT)编译性Java虚拟机 (JVM)字节码 (*.class 文件,特别是热点代码)机器码以机器码方式直接执行在 JVM

开发细节

  • Java 源文件以 ".java" 作为其扩展名,这些文件的主要构成部分是类 (class) 定义。

  • Java 应用程序的执行入口为 main() 方法。它的标准编写形式如下:public static void main(String[] args) {...}

  • 需注意,Java 语言对于大小写字母是严格区分的。

  • Java 方法是由一系列语句组成的,每一个语句都应以分号 ";" 结束。

  • 在一个 Java 源文件中,最多只能有一个 public(公共)类,但是其他类型的类(如私有类等)不受数量限制。

  • 如果一个源文件中包含了一个 public 类,那么该源文件必须以该公共类的名字命名。

安装 OpenJDK

JDK 说明

2019 年开始,Oracle JDK(Java Development Kit) 的商业用途需要付费。也就是说,如果你想要在生产环境中使用最新的 Oracle JDK,并想要获得官方提供的长期支持 (LTS,Long Term Support) 以及更新和补丁,你需要购买 Oracle 的订阅。

然而,Oracle 也提供了免费的 JDK 版本,即 Oracle OpenJDK。这个 JDK 的版本每三个月更新一次,但它不提供长期支持。此外,OracleJava SE 8 仍然在商业用途下免费,但这只适用于 20194 月或以前的版本。

对于个人,学术或研究用途,Oracle JDK 仍然是免费的。

请注意,若希望使用免费的 OpenJDK,并且需要长期的支持,也可以选择其他提供商的 JDK,例如:AdoptOpenJDK(更名为 Eclipse Adoptium),Amazon CorrettoAzul Zulu,或者 Red HatOpenJDK 等。这些版本一般也提供长期的支持,并且完全免费。

Adoptium OpenJDK

下面为大家提供一种免费预编译的 OpenJDK 部署方式,我们选择使用 Eclipse Adoptium 的产品来下载 OpenJDK

版本截图
最新 LTSimage.png
自选平台或版本image.png

如何查看真实的下载地址(GIF 动图操作)?

20231218-181441.gif

安装部署 Jdk1.8

  1. 在服务器上下载 OpenJDK
root@localhost:~# wget https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u392-b08/OpenJDK8U-jdk_x64_linux_hotspot_8u392b08.tar.gz

image.png

  1. 将其放置到 /usr/local 下:
root@localhost:~# tar -xf OpenJDK8U-jdk_x64_linux_hotspot_2023-12-17-03-51.tar.gz -C /usr/local/
  1. 更改下解包后的目录名:
root@localhost:~# mv /usr/local/jdk8u402-b04 /usr/local/jdk8
  1. /etc/profile 末尾添加以下内容:
root@localhost:~# cat << EOF >> /etc/profile
export JAVA_HOME=/usr/local/jdk8
export PATH=\$PATH:\$JAVA_HOME/bin
export CLASSPATH=.:\$JAVA_HOME/lib/dt.jar:\$JAVA_HOME/lib/tools.jar
EOF
  1. 重新载入环境变量配置:
root@localhost:~# source /etc/profile
  1. 检查 javajavac 工具:
# Java 运行环境
root@localhost:~# java -version
openjdk version "1.8.0_402-beta"
OpenJDK Runtime Environment (Temurin)(build 1.8.0_402-beta-202312161212-b04)
OpenJDK 64-Bit Server VM (Temurin)(build 25.402-b04, mixed mode)

# Java 编译器
root@localhost:~# javac -version
javac 1.8.0_402-beta

Hello World

  1. 编写 HelloWorld.java 代码:
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
  1. 编译与运行:
# 使用 javac 编译 Java 源代码文件
root@localhost:~# javac HelloWorld.java

# 编译成为 Java 字节码文件
root@localhost:~# ll HelloWorld.class
-rw-r----- 1 root root 427 Dec 18 18:41 HelloWorld.class

# 运行 HelloWorld.class 字节码文件
root@localhost:~# java HelloWorld
Hello, World!

快速入门

注释

Java 中有三种类型的注释:

  • 单行注释:
// 这是一个单行注释。
  • 多行注释或块注释:
/*
这是
一个多行
注释。
*/
  • 文档注释:这种类型的注释被 javadoc 工具用来从源文件中生成文档。
/**
* 这是一个文档化注释(DocComment)。
* @param name 一个代表名字的字符串
* @return String 一个代表问候的字符串
*/
public String sayHello(String name) {
    return "Hello, " + name;
}

变量

Java 在定义变量时遵循 “类型在前,名称在后” 的规则,与 C++C# 等语言一样。而元年后诞生的新语言,如 GoRust 则正相反,在定义变量时采取了 “名称在前,类型在后” 的规则,它们在设计之初使语法更贴近自然语言的表达习惯。

public class Main {
    public static void main(String[] args) {
        int age = 30;             // 整型变量在 Java 中默认占用4个字节
        double score = 90.6;      // 双精度浮点型变量在 Java 中默认占用8个字节
        char gender = '男';       // 字符型变量在 Java 中占用2个字节
        String name = "mystic";   // 字符串类型在 Java 中是一个引用类型,它的长度只受限于可用内存

        System.out.println("Age: " + age);
        System.out.println("Score: " + score);
        System.out.println("Gender: " + gender);
        System.out.println("Name: " + name);
    }
}

加号

Java 中,加号主要有两种用途,一种是作为数学加法运算符,一种是作为字符串连接运算符。

当一个数字和一个字符串相加时,数字将会被转换为字符串,然后再进行字符串连接。

public class Main {
    public static void main(String[] args) {
        // 加号作为数学加法运算符
        int a = 5;
        int b = 3;
        int sum = a + b;
        System.out.println("The sum is: " + sum);  // 输出 "The sum is: 8"

        // 加号作为字符串连接运算符
        String str1 = "Hello ";
        String str2 = "World";
        String message = str1 + str2;
        System.out.println(message);  // 输出 "Hello World"

        // 数字和字符串相加
        int num = 30;
        String str = "岁";
        String result = num + str;
        System.out.println(result);  // 输出 "30岁"
    }
}

数据类型

Java 中,每种数据都有其明确定义的数据类型。这种数据类型决定了在内存中为这个特定数据分配的具体空间大小(以 byte 为单位)。这不仅确保数据的安全,并且由于每种数据类型所需要的内存大小是固定的,Java 能够更有效地管理和使用内存。

数据类型 分类 具体 说明
基本数据类型 数值型 整型 byte 1 字节,取值范围:-128~127
short 2 字节,取值范围:-32768~32767
int 4 字节,取值范围:-2147483648~2147483647(Java 的默认整型)
long 8 字节,取值范围:-9223372036854775808~9223372036854775807
浮点型 float 4 字节,能表示大约 7 位有效数字的浮点数
double 8 字节,能表示大约 16 位有效数字的浮点数(Java 的默认浮点型)
字符型 char 2 字节,用于表示 Unicode 码点在 U+0000 到 U+FFFF 之间的字符
布尔型 boolean 布尔类型,只有两个取值:true 或 false
引用数据类型 类(Class) String 用于表示字符序列,非常常用
Integer 用于表示整数,是 int 类型的包装类
ArrayList 用于表示动态数组,能够自动调整其大小
Date 用于表示日期和时间
Etc. 类(Class)实际上可以定义任何类型的对象,包括系统内置类和用户定义类
接口(Interface) - 用来定义行为的类型,包含方法的签名,但不包含方法的实现
数组(Array) - 用于存储同一类型多个值的容器

类型转换

基本类型转换

自动类型转换(隐式类型)

自动类型转换通常发生在两种兼容类型之间,且目标类型大于源类型时。在这种情况下,数据将自动转换为更宽的类型,不会丢失信息。

int numInt = 100;
long numLong = numInt; // 自动类型转换,从 int 转为 long

强制类型转换(显式类型)

如果需要将兼容的类型进行转换,但目标类型小于源类型。这可能导致数据丢失。在这种情况下,需要使用强制类型转换。

double numDouble = 100.99;
int numInt = (int) numDouble; // 强制类型转换,从 double 转为 int

字符串的互转

Java 实际的项目开发中,String 与基本数据类型之间的转换是十分常见的,下面举例可供我们在代码中灵活使用。

基本数据类型转换为 String

使用 String.valueOf() 方法,这是最直接的方式。

int num = 456;
String str = String.valueOf(num);

使用 + 运算符,对于任何数据类型,Java 都会自动将其他数据类型与 String 相加的结果类型定为 String

int num = 456;
String str = num + "";

String 转换为基本数据类型

每一种基本数据类型(包含包装类型)都有对应的 parseXXX() 方法(将 String 转换为对应的基本数据类型)。

String str = "123";
int num = Integer.parseInt(str);

但请注意,parseXXX() 方法在处理不能转换为对应类型的字符串时,会抛出 java.lang.NumberFormatException 异常。

运算符

Java 中的常用运算符及其使用方法:

运算符 符号 说明
算术运算符 + 加法
- 减法
* 乘法
/ 除法
% 取余
++ 自增
-- 自减
关系运算符 == 等于
!= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于
逻辑运算符 && 逻辑与
|| 逻辑或
! 逻辑非
赋值运算符 = 简单赋值
+= 加等于
-= 减等于
*= 乘等于
/= 除等于
%= 取余等于
三元运算符 Boolean ? value1 : value2 如果 Boolean 为 true,结果为 value1,否则为 value2

Java 中运算符优先级的顺序(由高到低):

[] . ()                // 数组下标、点操作符、括号
! ~ ++ --              // 逻辑非、位非、自增、自减
* / %                  // 乘、除、取模
+ -                    // 加、减
<< >> >>>              // 左位移、右位移、无符号右位移
< <= > >= instanceof   // 小于、小于等于、大于、大于等于、检查是否为某类型
== !=                  // 等于、不等于
&                      // 逻辑与、按位与
^                      // 逻辑异或、按位异或
|                      // 逻辑或、按位或
&&                     // 短路与
||                     // 短路或
?=                     // 赋值、加等、减等、乘等、除等、取模等等

关键字

Java 中的关键字是一些预先定义的、具有特殊意义的单词,它们作为 Java 语言的基本构建块在 Java 代码中起着重要的作用。

以下是 Java 关键字的列表:

abstract, assert, boolean, break, byte, case, catch, char, class, const, continue, 
default, do, double, else, enum, extends, final, finally, float, for,
goto, if, implements, import, instanceof, int, interface, long, native, 
new, package, private, protected, public, return, short, static, 
strictfp, super, switch, synchronized, this, throw, throws, transient, 
try, void, volatile, while

需注意的是 constgoto 是保留关键字,但在 Java 中并未被使用。

另外,true, false, null 被称为字面量 (Literals),在某些情况下,它们也可以被视为关键字。

用户输入

Java 中,我们通常会使用 java.util.Scanner 类来获取用户输入。以下是一个示例:

import java.util.Scanner;  // 导入 Scanner 类

public class UserInput {
    public static void main(String[] args) {
        Scanner myObj = new Scanner(System.in);          // 创建一个 Scanner 对象
        System.out.println("Enter username");

        String userName = myObj.nextLine();              // 读取用户输入
        System.out.println("Username is: " + userName);  // 输出用户输入
    }
}

控制结构

顺序控制

Java 中,变量通常应在使用之前进行声明和初始化。Java 中定义成员变量时采用合法的前向引用。如:

public class ForwardReference {
    public static void main(String[] args) {
        System.out.println(value);  // 错误的前向引用,因为在打印它的值之前它尚未初始化
        int value = 10;
    }
}

分支控制

Java 中,有三种主要的分支控制结构:if 语句、switch 语句和 ?: (三元运算符)。

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

        // if 语句
        int num = 10;
        if (num > 0) {
            System.out.println("Number is positive.");
        } else if (num < 0) {
            System.out.println("Number is negative.");
        } else {
            System.out.println("Number is zero.");
        }

        // switch 语句
        int day = 3;
        switch (day) {
            case 1:
                System.out.println("Monday");
                break;
            case 2:
                System.out.println("Tuesday");
                break;
            case 3:
                System.out.println("Wednesday");
                break;
            // 其他 days...
            default:
                System.out.println("Invalid day");
        }

        // 三元运算符
        String result = (num > 0) ? "Positive" : "Not positive";
        System.out.println(result);
    }
}

循环控制

Java 中,有三种主要的循环结构:for 循环,while 循环和 do...while 循环。

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

        // for 循环
        System.out.println("FOR LOOP:");
        for (int i = 0; i < 5; i++) {
            System.out.println("The value of i is: " + i);
        }

        // while 循环
        System.out.println("\nWHILE LOOP:");
        int j = 0;
        while (j < 5) {
            System.out.println("The value of j is: " + j);
            j++;
        }

        // do...while 循环
        System.out.println("\nDO...WHILE LOOP:");
        int k = 0;
        do {
            System.out.println("The value of k is: " + k);
            k++;
        } while (k < 5);
    }
}

Java 中,breakcontinue 这两个重要的关键字,它们具有控制程序执行流程的能力。

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {

            // continue 用于在循环中直接开始下一次循环,忽略剩下的循环代码。
            if (j == 2) {
                continue; 
            }

            // break 语句通常用在循环或者 switch 语句中,用来立即结束当前的循环或者 switch,跳到下一条可执行代码。
            if (i == 3) {
                break;
            }
            System.out.println("The value of i is: " + i);
        }
    }
}

数组

Java 的数组是一个容器对象,它能够存储固定数量的具有相同类型的值。也就是说,数组的大小一旦创建,就不能改变。每一个存储在数组中的值可以通过索引进行访问。

声明数组

格式:

dataType[] arrayName;   // 推荐使用
dataType arrayName[];   // 也可以,但不常见

示例:

int[] myArray;

初始化数组

创建新数组:

myArray = new int[5];   // 它有5个元素,默认值是0

声明并初始化

在声明时直接初始化数组:

int[] myArray = new int[5];

也可以指定数组的具体内容:

int[] myArray = {1, 2, 3, 4, 5};

访问数组

可以通过数组的索引来访问数组的元素:

int firstElement = myArray[0];

也可以修改数组中的元素:

myArray[0] = 60;

获取数组长度

通过 length 属性,可以得到数组的长度:

int length = myArray.length;

遍历数组

可以使用 for 循环或 foreach 循环来遍历数组:

// 使用普通 for 循环遍历数组
for(int i = 0; i < myArray.length; i++) {
    System.out.println(myArray[i]);
}

// 使用 foreach 循环遍历数组
for(int num : myArray) {
    System.out.println(num);
}

引用传递

Java 中,数组是对象,所以在对数组赋值或复制时,是按引用传递的,而不是按值传递。

// 当你把一个数组标识符赋值给另一个数组标识符时,你只是复制了数组的引用,而不是数组本身。这意味着这两个引用指向的是同一个数组。
int[] array1 = {1, 2, 3, 4, 5};
int[] array2 = array1;

array2[0] = 10;

System.out.println(array1[0]);  // 输出 10

拷贝数组

如果想要在 Java 中得到数组的一个新副本,需要创建一个新的数组并复制每个元素,可以使用 System.arraycopyArrays.copyOf 方法。

public class Main {
    public static void main(String[] args) {
        int[] array1 = {1, 2, 3, 4, 5};
        int[] array2 = new int[array1.length];

        // 1. 使用 System.arraycopy 方法
        System.arraycopy(array1, 0, array2, 0, array1.length);

        // 2. 使用 Arrays.copyOf 方法
        // int[] array2 = Arrays.copyOf(array1, array1.length);

        array2[0] = 10;

        System.out.println(array1[0]);  // 输出 1
        System.out.println(array2[0]);  // 输出 10
    }
}

反转数组

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        System.out.println("原始数组:" + Arrays.toString(array));

        int temp = 0;
        int len = array.length;
        for (int i = 0; i < len / 2; i++) {
            temp = array[len - 1 - i];
            array[len - 1 - i] = array[i];
            array[i] = temp;
        }

        System.out.println("反转数组:" + Arrays.toString(array));
    }
}

二维数组

Java 中,二维数组其实就是数组的数组,通常可以用来表示一个表格或者矩阵。

初始化二维数组

int[][] array = new int[3][3];  // 3x3的二维数组,所有元素初始化为0

在声明的同时给二维数组赋值:

int[][] array = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

这个数组可以被看成一个 3x3 的矩阵。

访问二维数组元素

可以通过两个索引来访问二维数组的一个元素:

int value = array[2][2]; // 此时, value 的值是9

遍历二维数组

可以使用两层嵌套的 for 循环来遍历二维数组:

for (int i = 0; i < array.length; i++) {
    for (int j = 0; j < array[i].length; j++) {
        System.out.println(array[i][j]);
    }
}

面向对象

类(初阶)

在 Java 中,类(Class)是面向对象编程的基础,是创建对象的模板或蓝图,定义了一组具有相同属性(Fields)和行为(Methods)的对象。

属性与方法

Java 中,"属性"、"成员变量"或"字段"决定了类的"状态",而"方法"或"成员方法"决定了类的"行为"。这是理解面向对象编程的关键。

class Animal {
    // Fields (or instance variable)
    String name;
    int age;

    // Constructor
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Method
    public void eat() {
        System.out.println(name + " is eating.");
    }

    public void sleep() {
        System.out.println(name + " is sleeping.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Animal("Dog", 5);
        dog.eat();
        dog.sleep();

        Animal cat = new Animal("Cat", 3);
        cat.eat();
        cat.sleep();
    }
}

方法重载

Java 中,方法重载 (Overloading) 是指在同一个类中可以定义多个同名的方法,但参数的数量或类型必须不同。这就意味着,Java 允许我们用同一个方法名进行不同类型或数量参数的操作。

public class OverloadingExample {

    // 第一个 add 方法,两个参数都是 int 类型
    public static int add(int a, int b) {
        return a + b;
    }

    // 第二个 add 方法,参数是两个 double 类型变量
    public static double add(double a, double b) {
        return a + b;
    }

    public static void main(String[] args) {
        System.out.println(add(1, 2));
        System.out.println(add(1.0, 2.0));
    }
}

可变参数

Java 中,可以通过 "可变参数" (Varargs) 的方式让一个方法接受任意数量的参数。可变参数使用 ... 表示,并且它必须是方法签名中的最后一个参数。

public class Main {

    // Using varargs to list the favorite languages
    public static void favoriteLanguages(String name, String... languages) {
        if (languages.length == 0) {
            System.out.println(name + " doesn't have any favorite languages.");
        } else {
            System.out.print(name + "'s favorite languages include: ");
            for (String language : languages) {
                System.out.print(language + " ");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        favoriteLanguages("Mystic");                         // Outputs "Mystic doesn't have any favorite languages."
        favoriteLanguages("Mystic", "Go");                   // Outputs "Mystic's favorite languages include: Go"
        favoriteLanguages("Mystic", "Go", "Rust", "Python"); // Outputs "Mystic's favorite languages include: Go Rust Python"
    }
}

构造方法

构造方法又称为构造器 (constructor),是类的一种特殊的方法,它的主要作用是完成对新对象的初始化。它有以下几个特点:

  • 方法名和类名相同,且没有返回值。
  • 在创建对象时,Java 编译器会自动的调用该类的构造器完成对象的初始化。
  • 如果没有定义构造器,Java 编译器会提供一个默认的构造器。这个默认的构造器没有参数,而且什么都不做。
  • 一个类可以有多个构造器,只要它们的参数列表不同就可以,这叫做构造器重载。
public class Animal {
    String name;
    int age;

    // 构造器
    public Animal(String name, int age) {
        this.name = name;  // this.name 指的是类的字段,name则是构造器的参数
        this.age = age;    // this.age 指的是类的字段,age则是构造器的参数
    }
}

// 创建一个对象并初始化
Animal cat = new Animal("Tom", 3);

this 对象

Java 中,this 是一个引用变量,指向当前对象。在类的方法或构造器内部,可以使用 this 来引用当前对象,即调用该方法的对象。

引用当前类的实例变量

如果方法的参数和类的字段名相同,可以使用 this 关键字来引用字段。

public class Animal {
    // 字段
    String name;
    int age;

    // 构造器
    public Animal(String name, int age) {
        this.name = name;  // this.name 指的是类的字段,name则是构造器的参数
        this.age = age;    // this.age 指的是类的字段,age则是构造器的参数
    }
}

调用当前类的其他构造器

可以使用 this() 来调用同一个类中的其他构造器。一个构造器要调用另一个构造器,调用动作必须位于它的第一条语句中。

public class Animal {
    // 字段
    String name;
    int age;

    // 构造器1
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 构造器2
    public Animal() {
        this("unknown", 0);
    }
}

传递当前对象作为方法的参数

如果需要将当前对象作为参数传递给另一个方法,也可以使用 this 关键字。

public class TestUtils {
    public static void printAnimalName(Animal animal) {
        System.out.println("Animal's name is: " + animal.name);
    }
}

public class Animal {
    // 字段
    String name;

    // 构造器
    public Animal(String name) {
        this.name = name;
    }

    // 方法
    public void printName() {
        TestUtils.printAnimalName(this);
    }
}

在 Java 中,包(Package)是用来组织相关类和接口的一种机制。你可以把它们看作为文件夹在你的文件系统中的作用。每个 Java 类都位于一个包内,如果没有明确指定包,类就属于默认的无名包。

组织结构

Java 中,我们建立长的包名并按照三部分的约定(通常是 com.mycompany.myapp 或者 org.mycompany.myapp)进行组织,这是从全球范围确保我们的代码组织唯一性的一种方式。约定通常以 Internet 域名的反转开始,后面是公司名称和项目名称,是为了避免命名冲突。

comorgnet 等都是顶级域名,mycompany 代表你的公司或组织名称,myapp 是你的项目或应用名称。这样组织,可以在全球范围避免类名和包名的冲突,保证你的包名唯一。

当你在代码库,比如 GitHub,或者在大型项目中工作,或者你的代码需要被其他人或团队所使用,全局唯一性就显得更加重要。

关于 src 文件夹,srcsource 的缩写,代表你的源代码。这是编程中的一种常见结构,可以用于组织你的源代码文件。

src
|
└───com
    │
    └───mycompany
        │
        └───myapp
            │
            └───MyClass.java

包管理设计的槽点

在当今的软件开发环境中,Java 的包管理机制可能显得有些复杂,特别是当我们将其与 Go 等新一代语言的包管理方式进行对比时。Go 在设计时充分考虑了代码托管平台的应用,它直接使用 GitHub 仓库路径作为包的引用,这种方式做到了既现代化又简洁。

然而,我们需要理解 Java 的包机制最初设计于 20 多年前,那时候 GitHub 甚至尚未诞生。Java 选择基于文件系统的方式来组织代码。其实,许多 IDE(如 IntelliJ IDEAEclipse 等)都对这种结构提供了出色的支持,能够帮助用户自动创建和管理这样的目录结构。

声明包

Java 源文件的开头,用 package 声明语句指定类或接口所在的包:

package com.mycompany.myapp;

通常,包的命名遵循反转的公司域名的习惯,因为这样可以确保包名的唯一性。

使用包中的类

使用 import 语句来使用其它包中的类:

import com.mycompany.myapp.MyClass;

使用同一包中的所有类,可以使用 *

import com.mycompany.myapp.*;

如果不想使用 import,也可以在实际位置 “按需引入”,使用类的完全限定名,包括包名和类名:

com.mycompany.myapp.MyClass myObject = new com.mycompany.myapp.MyClass();

创建包

当编译有 package 声明的 Java 文件时,编译器会创建一个对应的目录结构。例如,有以下的源文件:

package com.mycompany.myapp;

public class MyClass {
}

然后运行 javac com/mycompany/myapp/MyClass.java,编译器会在当前目录下生成 com/mycompany/myapp/MyClass.class

OOP 思想

封装

封装是一种将数据(变量)和数据的操作(方法)绑定在一起的机制,同时隐藏了对象的内部实现细节。

封装的优点

  • 增强安全性:封装确保了对象中重要的数据被隐藏在类中,防止了非法直接访问。比如,我们可以把字段设置为 private ,这样它们只能通过类中的公开的方法(被称为 gettersetter)来访问和修改。
  • 简化代码维护:封装使得我们对对象的内部实现进行修改时不会影响到使用这个对象的其他代码,只需要知道对象提供了哪些公开的方法和调用方式。

示例代码

Java 中,对属性的封装通常可以有 SetterGetter 方法,当然还可以有更高级复杂的封装,以满足特定的业务需求。

public class Person {
    private String name;  // 将 name 字段私有化,防止外部直接访问

    // Setter 方法,用于设置 name 的值,可以添加条件判断、数据校验逻辑
    public void setName(String name) {
        if (name != null && !name.isEmpty()) {
            this.name = name;
        }
    }

    // Getter 方法,用于获取 name 的值
    public String getName() {
        return name;
    }
}

继承

继承是一个类(称为子类或派生类)可以获取另一个类(称为父类或基类)的字段和方法。

继承的优点

  • 代码重用:子类继承了父类的属性和方法,避免了代码的重复编写。
  • 提供了多态性的基础:多态性是面向对象编程的另一个重要特性,允许子类对象可以替代父类对象使用。

示例代码

Java 中,继承是通过 extends 关键字来实现的。

// 父类
public class Animal {
    public void eat() {
        System.out.println("The animal eats");
    }
}

// 子类
public class Cat extends Animal {
    public void meow() {
        System.out.println("The cat meows");
    }
}

// 在主方法中使用
public static void main(String[] args) {
    Cat myCat = new Cat();
    myCat.eat();  // From Animal class
    myCat.meow(); // From Cat class
}

多态

多态允许把不同类型的对象当做其超类的对象对待。也就是说,如果一个类是另一个类的子类,那么可以使用一个父类引用来指向一个子类对象。

Java 中,实现多态有两种方式:方法重写/方法覆盖 (Override) 和接口 (Interface),利用多态的特性可以使我们编写更通用、更灵活的代码。

类(中阶)

访问修饰符

Java 中,访问修饰符是一种关键字,用于设置其他类对某个类或其成员(包括字段和方法)访问权限的等级。Java 提供了四种访问修饰符:

  • public:公开的。如果一个类,方法或者字段被声明为 public,那么它可以被任何其它类访问。
  • protected:受保护的。protected 成员可以被同一个包内的任何类访问,也可以被子类访问,无论这个子类是否处于同一包中。
  • default(也称为 package-private):包内私有的。如果成员没有明确声明访问修饰符(也就是默认的,被称为 defaultpackage-private),那么它只能被同一个包里的类访问。
  • private:私有的。private 成员只能被同一类访问。

访问修饰符的使用可以实现封装。通过限制类的内部数据和实现代码的访问,可以使代码更安全,更容易维护,且更改起来更方便,因为只需要关注与该类交互的其他类所看到的公开接口。

super 关键字

调用父类的构造方法

在定义子类的构造方法时,使用 super 关键字调用父类的构造方法。Java 中的每一个构造方法的第一行都会默认调用父类的无参构造方法,如果父类没有无参构造方法,子类就必须用 super 关键字明确地调用父类的其他构造方法。

public class ChildClass extends ParentClass {
    public ChildClass() {
        super();      // 调用 ParentClass 的无参构造方法
    }

    public ChildClass(String name) {
        super(name);  // 调用 ParentClass 的有参构造方法
    }
}

调用父类的属性和方法

当子类需要访问父类的属性和方法(其中包括被子类重写的方法)时,可以使用 super 关键字。

public class ChildClass extends ParentClass {
    public void display() {
        super.display();  // 调用父类的 display 方法
    }
}

方法重写

当子类重写了父类的方法时,子类对象使用这个被重写的方法,就表现出了多态性。

public class Animal {
    public void makeSound() {
        System.out.println("The animal makes a sound");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("The cat meows");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal myAnimal = new Cat();
        myAnimal.makeSound();  // Outputs "The cat meows"
    }
}

接口

类实现接口

interface Animal {
    void eat();
}

class Dog implements Animal {
    public void eat() {
        System.out.println("Dog eats meat");
    }
}

class Cat implements Animal {
    public void eat() {
        System.out.println("Cat eats fish");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        a1.eat();
        Animal a2 = new Cat();
        a2.eat();
    }
}

继承接口

interface Animal {
    void eat();
}

interface Pet extends Animal {
    void play();
}

class Dog implements Pet {
    public void eat() {
        System.out.println("Dog eats meat");
    }
    public void play() {
        System.out.println("Dog play ball");
    }
}

public class Test {
    public static void main(String[] args) {
        Pet p = new Dog();
        p.play();
        p.eat();
    }
}

向上/下转型

首先,定义一个父类和子类。例如:

class Animal {
    public void eat() {
        System.out.println("Animal eat");
    }
}

class Dog extends Animal {
    public void eat() {
        System.out.println("Dog eat");
    }
    
    public void bark() {
        System.out.println("Dog bark");
    }
}

向上转型(Upcasting)

向上转型:将一个子类对象转型为父类类型。在这个示例中,将 Dog 类型的 d 转型为 Animal 类型。

Dog d = new Dog();
Animal a = d;      // 向上转型
a.eat();           // 输出 "Dog eat"

向下转型(Downcasting)

向下转型:将父类类型对象转型为子类类型。在这个示例中,将 Animal 类型的 a 显式转型为 Dog 类型,这样就可以调用 Dog 类中特有的 bark 方法了。

Animal a = new Dog();
Dog d = (Dog)a;    // 显式的向下转型
d.bark();          // 输出 "Dog bark"

注意:向下转型可能存在风险。如果实际的对象类型和所需要转型的目标类型不一致,虽然在编译阶段不会报错,但在运行时会抛出 ClassCastException 异常。

动态绑定机制

  • 静态绑定:静态绑定在编译时发生,这是因为在编译时已经确定了被调用的方法或者变量。静态绑定通常发生在访问 static 方法,final 方法或者构造器时。这些方法在编译时就确定了,不会在运行时改变。

  • 动态绑定:动态绑定则在运行时发生,其中最常见的例子就是方法的重载和重写。动态绑定有助于实现多态,在运行时确定调用哪个方法。

class Animal {
    void eat() {
        System.out.println("animal is eating...");
    }
}

class Dog extends Animal {
    void eat() {
        System.out.println("dog is eating...");
    }
}

// 虽然 a 的类型是 Animal,或许会认为调用 a.eat() 会调用 Animal 类的 eat() 方法。但实际上因为 a 实际引用的对象是 Dog,Java 会动态地决定在运行时调用 Dog 类的 eat() 方法。这就是动态绑定的一种体现。
public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.eat();   // Dog 的 eat 方法被调用,体现了动态绑定
    }
}

类(高阶)

类变量和类方法

类变量

也称为静态变量,通过在变量声明前面加上关键字 static 来定义。它们不依赖于任何实例,直接通过类就可以访问。这意味着无论创建类的多少对象,类变量都只有一份,类的所有实例都共享这个变量。

class MyClass {
    static int numInstances = 0; // numInstances 是类变量
}

类方法

也称为静态方法,和类变量一样,类方法也是通过关键字 static 来定义的。类方法可以直接通过类来访问,而不需要创建类的实例。

class MyClass {
    static void myMethod() {     // myMethod 是类方法
        System.out.println("Class method called");
    }
}

注意:类方法只能操作类变量,不能操作类的实例变量,而且它不能使用 this 关键字,因为 this 关键字指向类的当前实例,而类方法是不依赖于实例的。

理解 mian 方法

Java 程序的入口是 main 方法。在一个 Java 程序中,main 方法是 JVMJava 虚拟机)执行的第一个方法。

public static void main(String[] args) {
    // your code...
}

让我们来逐一解释语法:

  • public:这是访问修饰符,表示该方法可以被所有其他类访问。
  • static:这意味着这个方法属于类,而不是类的一个实例。事实上,在 JVM 调用 main 方法时,还没有创建任何类的实例。
  • void:这表示该方法没有返回值。
  • main:这是方法的名称,由 JVM 特别识别。这是程序的入口点。
  • String[] args:这是 main 方法的参数,args 是一个字符串数组,可以接受运行 Java 程序时的命令行参数。

代码块

普通代码块

Java 中,代码块(或称为块)是由大括号 { } 括起来的一组语句。(空的)代码块本身并不会执行任何动作。但当它与控制流语句(如 if-thenfor 等)一起使用时,代码块中的所有语句将作为一组,按照控制流的规则进行执行。

int number = 5;

if(number > 0) { 
    // This is a block
    System.out.println("Number is positive.");
    System.out.println("This will only run if the condition is true.");
}

特殊代码块

  • 静态代码块:使用 static 关键字定义,当 JVM 加载类时会自动执行静态代码块。这主要用于初始化类变量。
  • 实例初始化块:没有任何前导关键字,当创建类的对象时会执行。这主要用于初始化实例变量。
// 当类被加载到 JVM 中时,首先执行静态代码块;然后当类的对象被创建时,先执行实例初始化块,然后才执行构造方法。
class MyClass {
    static {
        // This is a static block
        System.out.println("Static block is executed");
    }
    
    {
        // This is an instance block
        System.out.println("Instance block is executed");
    }
    
    MyClass() {
        System.out.println("Constructor is executed");
    }
}

final 关键字

修饰类

final 用来修饰一个类时,表示这个类不能被继承。也就是说,没有其他的类可以继承这个 final 类。

final class FinalClass {
    ...
}

修饰方法

final 用来修饰一个方法时,表示这个方法不能被子类重写 (Override)。但是可以被子类继承和直接使用。

public class MyClass {
    final void finalMethod() {
        ...
    }
}

修饰变量

final 用来修饰一个变量时,表示这个变量的值一旦被赋值后,就不能被修改,就成了常量。对于引用类型的变量,final 保证了引用不改变,但是引用指向的对象的状态是可以改变的。

final int finalVar = 10;               // 现在 finalVar 就不能被修改了
final MyClass myClass = new MyClass(); // 引用不能改变,但 myClass 的对象内部状态可以改变

抽象类

Java 中,抽象类 (abstract class) 是一种特殊的类,它不能被实例化。也就是说,不能使用 new 关键字创建一个抽象类的对象。抽象类主要是作为其他类的基础类 (superclass) 被用来创建子类。

创建抽象类的目的是为了定义一些可能存在于一系列子类中的共享行为或者状态,以及一些可能由子类具体实现的抽象行为。我们可以在抽象类中定义变量和方法,其中的方法可以是抽象方法或者非抽象方法。

Java 中,创建抽象类需要使用 abstract 关键字:

public abstract class Animal {
    public abstract void sound();      // 定义一个抽象方法:抽象方法是没有实现体的方法,只有方法签名,以分号结束。
  
    public void breathe() {            // 定义一个非抽象方法
        System.out.println("Breathing...");
    }
}

子类可以继承抽象类,并需要提供抽象方法的实现。如果子类不能或者不想提供抽象方法的实现,那么子类也必须声明为抽象的。

public class Dog extends Animal {
    public void sound() {
        System.out.println("Woof woof...");
    }
}

抽象类主要用于面向对象的设计和编程,尤其是在需要利用继承和多态的时候。另外,抽象类常和设计模式中的模板方法模式搭配使用。

接口

Java 中的接口 (interface) 是一种引用类型,它是方法的集合。接口提供了一种方式来确定一个类应该做什么,同时不指定它如何去做。使用接口可以增强代码的可复用性和可测试性。

类实现接口

interface Animal {
    void eat();
}

class Dog implements Animal {
    public void eat() {
        System.out.println("Dog eats meat");
    }
}

class Cat implements Animal {
    public void eat() {
        System.out.println("Cat eats fish");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        a1.eat();
        Animal a2 = new Cat();
        a2.eat();
    }
}

继承接口

interface Animal {
    void eat();
}

interface Pet extends Animal {
    void play();
}

class Dog implements Pet {
    public void eat() {
        System.out.println("Dog eats meat");
    }
    public void play() {
        System.out.println("Dog play ball");
    }
}

public class Test {
    public static void main(String[] args) {
        Pet p = new Dog();
        p.play();
        p.eat();
    }
}

内部类

成员内部类(Member inner class)

最常见的类型,一个类定义在另一个类的内部,就像是外层类的一个成员。

public class OuterClass {
    private int data = 0;
 
    class InnerClass {
        public void print() {
            System.out.println(data);        // 可以访问外部类的私有变量
        }
    }
}

静态内部类(Static inner class)

与成员内部类类似,但是静态内部类不能直接访问外部类的非静态成员,除非显式传递引用。

public class OuterClass {
    private static int data = 0;
 
    static class InnerClass {
        public void print() {
            System.out.println(data);        // 只能访问外部类的静态成员
        }
    }
}

方法内部类(Local inner classes)

定义在方法内部,只能在定义该内部类的方法内实例化并调用函数,不能在此方法外对其进行引用。

public class OuterClass {
    private int data = 0;

    public void someMethod() {
        class InnerClass {
            public void print() {
                System.out.println(data);    // 可以访问外部类的私有变量
            }
        }
        
        InnerClass inner = new InnerClass();
        inner.print();
    }
}

匿名内部类(Anonymous inner class)

匿名内部类是一种特殊的内部类,它没有类名。通常用于只用一次的场合。最常用在创建线程或者实现回调函数。

public class OuterClass {
    public void someMethod() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello, World");
            }
        }).start();
    }
}

编程进阶

枚举

在 Java 中有两种方式实现枚举类型:一种是使用 enum 关键字,另一种是通过创建一个包含私有构造函数的类。

使用 enum 枚举

Java 中,枚举 (Enum) 类型是一种特殊的类类型,用于定义固定的常量值。枚举提供了一种出色的方式来创建一组定义好的常量,例如一周的天数、季节、方向等。

定义枚举类型:

public enum Season {
    SPRING, SUMMER, FALL, WINTER
}

可以像使用其他数据类型一样使用枚举,比如:

Season season = Season.SPRING;

枚举本质上就是一个类,因此它们也可以有构造器、方法和属性。比如:

public enum Season {
    SPRING("Spring"),
    SUMMER("Summer"),
    FALL("Fall"),
    WINTER("Winter");

    private String name;

    Season(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

使用枚举的好处是它们是类型安全的,换句话说,你不能让一个变量引用未定义的枚举值。另外,它们在内存中只存在一份,所以在对常量值的比较等操作上比字符串等其他方式更加高效。

自定义类实现枚举

Java 5 之前,枚举通常是通过创建一个类来实现的,这个类包含一组预定义的实例,并防止创建额外的实例。这种方式会比使用 enum 关键字更复杂一些,作为了解即可。

public class Season {
    public static final Season SPRING = new Season("Spring");
    public static final Season SUMMER = new Season("Summer");
    public static final Season FALL = new Season("Fall");
    public static final Season WINTER = new Season("Winter");

    private final String name;

    private Season(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

常用类

Java 提供了许多常用的类库,下面列出了一些常用的 Java 类:

类名描述
ObjectJava 中所有类的父类,定义了一些所有对象都有的基本行为
包装类(如 Integer, Double, Boolean 等)将基本数据类型包装成对象
String用于操作字符串
StringBuilder / StringBuffer可变的字符序列,常用于处理字符串拼接操作
Math提供了一些用于执行基本数学运算的方法
System提供了许多和系统交互的方法
日期和时间类(如 LocalDate, LocalTime, LocalDateTime 和 Date 等)用于处理日期和时间
集合类(如 ArrayList, LinkedList, HashSet, HashMap 等)用于存储和操作数据的集合
文件 I/O 类(如 File, FileInputStream, FileOutputStream, BufferedReader, BufferedWriter 等)用于文件操作和 I/O 操作
线程类(如 Thread, Runnable 等)用于处理线程和并发相关的操作

包装类

Java 中,每个基本数据类型都有一个对应的包装类。这些类封装了基本数据类型,使得它们可以被视为对象。以下是基本类型和它们的对应包装类:

  • boolean 对应 Boolean
  • char 对应 Character
  • byte 对应 Byte
  • short 对应 Short
  • int 对应 Integer
  • long 对应 Long
  • float 对应 Float
  • double 对应 Double

包装类主要有两个作用:

  1. 作为和基本数据类型对应的类,使得我们可以在需要对象的地方使用基本数据类型。例如,你不能将基本数据类型放入一个 ArrayList,但是你可以将它们的包装类放入列表。
  2. 提供了一系列静态方法,用于实现基本数据类型与字符串之间的转换、获取基本数据类型的最大或最小值等。

集合类

Java 中,集合类是 Java Collections Framework 的一部分,它包含了一系列接口和类,这些接口和类被用来处理一组对象。

Java 集合框架主要包括以下几种类型:

  1. List:一个有序集合,可以包含重复的元素。可以通过索引来访问和修改元素。常见的 List 实现类有 ArrayListLinkedListVector 等。
  2. Set:一个不包含重复元素的集合。它不包含任何索引,因此只能通过迭代器来访问和修改元素。常见的 Set 实现类有 HashSetLinkedHashSetTreeSet 等。
  3. Queue:按照先进先出 (FIFO) 的原则来保存元素的集合。也可以按照优先级来排序元素。常见的 Queue 实现类有 LinkedListPriorityQueue 等。
  4. Map:保存键值对的集合。键不能重复,每个键都可以映射到一个值。常见的 Map 实现类有 HashMapLinkedHashMapTreeMap 等。

以上这些接口和它们的实现类提供了丰富的数据结构和操作,可以帮助我们应对各种复杂的编程需求。

泛型

泛型语法

Java 泛型是 Java SE 5.0 中引入的一个新特性,允许在类、接口和方法中使用类型参数。主要优点包括编译时类型安全、消除类型转换等。

使用 Java 泛型的核心原理是参数化类型,即所操作的数据类型被指定为一个参数。就像方法可以接受不同的参数,类、接口及方法现在也可以接受参数。

比如,我们可以创建一个可以存储任何类型数据的 Arraylist,像这样: ArrayList<String>ArrayList<Integer>

这个 StringInteger 称为类型参数,并且可以在使用类或方法时指定。

基本的 Java 泛型语法如下:

class ClassName<T1, T2, ..., Tn> { /* ... */ }

内置泛型类

Java 有许多内置的支持泛型的类和接口。这些主要在集合框架中,比如:

  • ArrayList
  • LinkedList
  • HashSet
  • TreeSet
  • PriorityQueue
  • HashMap
  • LinkedHashMap
  • TreeMap

除了这些,在 Java 的其他部分也有支持泛型的类,比如 Optional<T>Stream<T>Future<T> 等。

下面是使用 ArrayList 的基本示例:

import java.util.ArrayList;

public class Sample {
    public static void main(String[] args) {
        ArrayList<String> stringList = new ArrayList<>();
        stringList.add("Hello");
        stringList.add("World");
        for (String str : stringList) {
            System.out.println(str);
        } // 输出: Hello  World
    }
}

自定义泛型类

一个泛型参数

public class Box<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>();
        integerBox.set(10);
        Integer someInteger = integerBox.get();
        System.out.println(someInteger);  // 输出: 10

        Box<String> stringBox = new Box<>();
        stringBox.set("Hello World");
        String someString = stringBox.get();
        System.out.println(someString);  // 输出: Hello World
    }
}

多个泛型参数

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public K getKey() {
        return key;
    }

    public void setValue(V value) {
        this.value = value;
    }

    public V getValue() {
        return value;
    }

    public static void main(String[] args) {
        Pair<Integer, String> pair = new Pair<>(1, "Hello");
        System.out.println(pair.getKey());  // 输出: 1
        System.out.println(pair.getValue());  // 输出: Hello

        Pair<String, String> dictionary = new Pair<>("Hello", "你好");
        System.out.println(dictionary.getKey());  // 输出: Hello
        System.out.println(dictionary.getValue());  // 输出: 你好
    }
}

自定义泛型接口

文件 "Transformer.java":

// 泛型接口定义
public interface Transformer<T, U> {
    U transform(T input);
}

文件 "IntegerToStringTransformer.java":

// 泛型接口实现
public class IntegerToStringTransformer implements Transformer<Integer, String> {
    @Override
    public String transform(Integer input) {
        return Integer.toString(input);
    }

    public static void main(String[] args) {
        IntegerToStringTransformer transformer = new IntegerToStringTransformer();
        System.out.println(transformer.transform(123));  // 输出: "123"
    }
}

自定义泛型方法

import java.util.Arrays;

public class GenericArrayCreator {

    public static <T> T[] createArray(T... items) {
        return items;
    }

    public static void main(String[] args) {

        // 使用Integer
        Integer[] integerArray = GenericArrayCreator.createArray(1, 2, 3, 4, 5);
        System.out.println(Arrays.toString(integerArray));  // 输出: [1, 2, 3, 4, 5]

        // 使用String
        String[] stringArray = GenericArrayCreator.createArray("Hello", "World");
        System.out.println(Arrays.toString(stringArray));   // 输出: [Hello, World]

        // 使用Double
        Double[] doubleArray = GenericArrayCreator.createArray(1.1, 2.2, 3.3);
        System.out.println(Arrays.toString(doubleArray));   // 输出: [1.1, 2.2, 3.3]
    }
}

注解

注解(Annotation)是 Java 提供的一项特性,用来给代码添加元数据,这些元数据可以在编译时或运行时通过反射机制读取和处理。所谓的元数据,就是关于数据的数据,比如方法定义、类定义、字段定义等。

注解的用途

  1. 生成文档:这是最常见的,也是 Java 最早提供的注解。比如,JavaDoc@param 来注释方法参数。
  2. 编译检查:比如 @Override 放在方法上,如果你的方法并没有覆盖父类方法,编译器会发出警告。
  3. 代码分析:比如,可以使用一些工具进行代码分析,了解代码的质量。
  4. 编译时动态处理,生成其他代码。
  5. 运行时动态处理:比如可以写一个标记需要测试的方法的注解,在运行时动态加载并运行这些方法。

内置注解类型

@Override

该注解只能用于方法。当我们希望覆盖超类中的方法时,可以使用此注解来告诉编译器我们的意图。编译器将检查超类是否存在需要被覆盖的方法。这可以防止因拼写错误或者方法签名不一致导致的问题。

public class MySubClass extends MySuperClass {
    @Override
    public void someMethod() {
        //...
    }
}

@Deprecated

该注解可以用于方法、类、属性等,用于表示被修饰的元素已经过时,不推荐使用。如果使用了被此注解标记的元素,编译器会给出警告。

@Deprecated
public class MyObsoletedClass {
    // ...
}

@SuppressWarnings

该注解用于告诉编译器忽略特定的警告。比如,或许已经知道代码中有一些过时的使用或者未经检查的类型转换,但仍然需要使用它们,那么就可以使用此注解来禁止那些特定的警告。

@SuppressWarnings("deprecation")
public void useDeprecatedMethod() {
    // 调用了一个被 @Deprecated 标记的方法
    myObject.deprecatedMethod(); 
}

元注解

Java 中,有四种元注解(对注解进行注解),它们用于提供注解的行为信息:

  1. @Target:表示注解可以应用的位置,比如类、方法、字段等。
  2. @Retention:表示注解在哪个级别可用,如源码级 (SOURCE)、类文件级 (CLASS) 或者运行时级 (RUNTIME)
  3. @Documented:指示将此注解包含在 javadoc 中,它代表着此注解会被 javadoc 工具提取成文档。
  4. @Inherited:表示注解可以被子类继承。

这些元注解被用来注解其他的注解定义。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
    String value() default "";
}

自定义注解

下面定义了 MyAnnotation,然后在 TestExample 类的 test 方法上应用了该注解。在 main 方法中,使用反射 API 获取了 test 方法上的 MyAnnotation 注解并打印出它的 value 元素的值。

import java.lang.annotation.*;
import java.lang.reflect.Method;

// 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyAnnotation {
    String value() default "";
}

public class TestExample {

    // 使用注解
    @MyAnnotation(value="This is a test.")
    public void test() {
        // ...
    }

    public static void main(String[] args) throws NoSuchMethodException {
        TestExample example = new TestExample();
        example.test();

        // 使用反射获取注解数据
        Method method = TestExample.class.getMethod("test");
        MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
        System.out.println("Annotation Value: " + annotation.value());
    }
}

异常处理

Java 中的异常处理是一种强大的机制,用于处理在程序执行过程中可能出现的错误情况。Java 提供了 try、catch、finally 和 throw 关键字来处理异常。

异常处理语句

  • try-catchtry-catch 语句块用于捕获可能在 try 语句块中出现的异常。catch 中指定的异常类型决定了它可以处理哪些异常。
// try-catch 结构中的多个 catch 块的行为方式与 if-elif 结构相似。
// 一旦在 try 块中抛出了异常并被某个 catch 块捕获处理,其他的 catch 块,即使它们也能匹配到这个异常,也将不会被执行。
try {
    // 一些可能会抛出异常的代码
} catch (ExceptionType1 e) {
    // 处理 ExceptionType1 异常的代码
} catch (ExceptionType2 e) {
    // 处理 ExceptionType2 异常的代码
}
  • finally:无论是否发生异常,finally 语句块中的代码都会被执行。这通常用于进行清理工作,比如关闭打开的文件或数据库连接。
try {
    // 一些可能会抛出异常的代码
} catch (Exception e) {
    // 处理异常的代码
} finally {
    // 无论是否发生异常,都会执行的代码
}

throw 手动抛出

可以使用 throw 关键字手动抛出一个异常,这个异常可以是 Java 自带的异常类也可以是自定义的异常。

try {
    throw new ArithmeticException("Division by zero!");
} catch (ArithmeticException e) {
    System.out.println(e.getMessage());  // 打印:Division by zero!
}

自定义异常

可以通过继承 Exception 类(用于可检查异常)或 RuntimeException 类(用于运行时异常)来创建自定义异常。

public class CustomException extends Exception {
    public CustomException(String message) {
        super(message);
    }
}

// Elsewhere in the code...
throw new CustomException("This is a custom exception!");

五大运行时异常

Java 中,常见的五种运行时异常,它们都是 RuntimeException 的子类:

  • NullPointerException:当试图访问 null 对象的成员时抛出。
  • ArrayIndexOutOfBoundsException:当试图访问数组的非法索引时抛出。
  • ClassCastException:当试图将对象强制转型为不适合的类型时抛出。
  • ArithmeticException:当出现非法的算术条件时抛出,例如除数为零。
  • NumberFormatException:当试图将字符串转换为数值类型,但该字符串的格式不适合转换时抛出。

多线程

Java 提供了对多线程编程的原生支持。多线程是指在一个程序中有两个或多个并发执行的线程,这可以在程序中同时执行多项任务。在 Java 中,主要有两种方法来创建线程。

继承 Thread 类

可以创建一个新类,让它继承自 Thread 类,然后覆写该类的 run 方法。然后你可以创建这个类的对象,调用它的 start 方法来启动新线程。

class MyThread extends Thread {
    public void run() {
        System.out.println("This is a new thread.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

实现 Runnable 接口

可以创建一个新类,让它实现 Runnable 接口,并覆写接口中的 run 方法。然后你可以把这个类的对象作为参数传递给 Thread 类的构造方法来创建新线程。

这种方法(实现 Runnable 接口)更为常用,因为它允许我们的类可以继承其他类。

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("This is a new thread.");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

注意,调用 start 方法后,线程的生命周期就开始了,Java 虚拟机会自动地调用线程对象的 run 方法。

除此之外,Java 还提供了很多其他用于多线程编程的特性和工具,比如 synchronized 关键字用于线程同步,wait 和 notify 用于线程间的通信,以及 java.util.concurrent 包中的高级并发工具等。

IO 流

在 Java 中,有许多类可以帮助我们在程序中进行文件读写操作。读文件时,通常使用 FileReader、BufferedReader 或 Scanner;写文件时,通常使用 FileWriter 或 PrintWriter。

文件读取

代码示例会打开指定路径的文件,然后按行读取文件内容,并打印到控制台。

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        String filePath = "path_to_your_file";
        try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

文件写入

代码示例会打开指定的文件(若文件不存在,则会创建新文件),然后向文件写入多行字符串。

import java.io.FileWriter;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        String filePath = "path_to_your_file";
        // try (FileWriter writer = new FileWriter(filePath, true)) {  // 第二个参数为:续写,追加模式
        try (FileWriter writer = new FileWriter(filePath)) {
            writer.write("Hello, World!\n");  // 第一行
            writer.write("This is the second line.\n");  // 第二行
            writer.write("And this is the third line.\n");  // 第三行
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

反射

Java 的反射机制是一种非常强大的工具,它允许运行中的 Java 程序进行自我检查,并对类、接口、字段和方法进行操作。这就意味着,你可以在运行时获取任何类的内部信息,并能直接操作任何对象的内部属性以及方法。

反射应用

  • 创建对象: 反射可以用来在运行时创建和操作对象。即使你没有类的对象,也可以使用反射来创建类的对象,只需知道它的完全限定类名。
  • 获取类信息: 反射可以获取运行时类的完整结构,包括类名、父类、接口、构造器、方法、字段等。
  • 调用方法: 反射可以在运行时调用对象的任何方法,哪怕这个方法是私有的。
  • 操纵字段: 反射还可以在运行时访问和修改对象的字段,即使它们是私有的。

简单示例

通过反射创建对象和调用方法:

public class Example {
    public void show() {
        System.out.println("Hello, World!");
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> cls = Class.forName("Example");             // 获取Example类的Class对象
        Object obj = cls.newInstance();                      // 创建Example的对象
        Method method = cls.getDeclaredMethod("show");       // 获取Example类的show方法
        method.invoke(obj);                                  // 调用show方法
    }
}

LTS 新特性

Oracle Java SE 版本支持路线图:www.oracle.com/java/techno…

image.png

Java 8

  • 引入了 Lambda 表达式(Java 的函数式编程)。
  • 添加了 Stream API,用于在集合对象上进行复杂操作。
  • 引入了新的日期和时间 APIjava.time 包)。
  • 添加了 Optional 类,用于解决空指针异常问题。

Lambda 表达式

Stream API

新日期时间 API

接口默认方法

Java 11

  • 新增了 var 关键字,用于简化局部变量的类型推断。
  • 新的 HTTP/2 客户端 API,用于改善请求 HTTP 服务器的性能。

Java 17

  • 引入了密封类 (Sealed Classes),用于限制类的继承。

Java 21

  • 引入了虚拟线程,虚拟线程是轻量级线程,可大大减少编写、维护和观察高吞吐量并发应用程序的工作量。

  • 引入一种新的集合接口,用于表示具有明确定义顺序的集合。该集合有明确的每一个元素,同时还提供统一的 API 来访问首、尾元素,以及反转元素。

image.png

参考教程

《韩顺平 零基础 30 天学会 Java》