概念
Java 是由 Sun 公司于 1995 年 5 月推出的一门面向对象的编程语言,具有跨平台、健壮性、简单性等诸多特点。主要运用在web端、服务器端的开发中。
JVM JRE JVM
JVM(Java Virtual Machine)
JVM
是Java语言的核心组件之一,JVM
是一个在物理计算机上执行字节码的虚拟计算机。它的作用是在运行时将字节码加载到内存,并进行解释和执行。
在不同平台上安装不同的JVM
,由不同平台的JVM
虚拟机负责,将字节码编译为该平台能执行的机器码。这实现了Java程序的跨平台性,即有了一次编译、到处运行的说法。
JVM
也负责内存管理,减少了程序员手动管理内存的工作。
JVM
提供了强大的异常处理机制,可以捕获程序中的异常,确保程序运行中的稳定性和可靠性。
JRE(Java Runtime Environment)
JRE
是Java运行时的环境的缩写,JRE
包含JVM
以及Java类库和其他支持文件。JRE
提供了计算机上运行Java程序所需要的一切。
JDK(Java Development Kit)
JDK
被称为开发Java应用程序的软件工具包,JDK
包含了JRE
以及用于编译、调试、和运行的Java开发工具,如编译器(javac
)、调试器(jdb
)等。
Java程序的执行
- Java编译器将源程序编译为class文件
- Java虚拟机将class文件加载到内存
- Java虚拟机进行合法性和安全性检测后,解释字节码并执行
编译型语言 解释型语言
我们通过高级语言编写的程序,计算机无法识别,需要我们将写好的程序先翻译成计算机听得懂的语言,它才能够去执行。
编译型和解释型是两种常见的编程语言执行方式。
- 在编译型语言中,源代码在程序执行之前,先通过编译器将源代码编译为本地机器码,生成一个可执行文件,这个文件可以在特定的平台上运行。典型的编译型语言有C、C++。
- 在解释型语言中,源代码在程序运行时,通过解释器逐句读取,并将其转换为可执行代码,然后执行。这个过程是在程序运行时动态进行的,不生成独立的可执行文件。典型的编译型语言有Python、JavaScript。
编译型语言和解释型语言各有其优势,根据需求和项目的特点进行选择。
面向过程基础
Java开发环境
-
JDK下载与安装
**JDK下载: **Java Downloads | Oracle 中国
-
配置环境变量
将Java环境添加到系统环境变量Path里。
在CMD(命令提示符)中键入Java -version命令,如果打印出您安装的Java版本号,就代表Java环境变量配置成功。
-
推荐软件
-
Visual Studio Code
一款轻量级跨平台编辑器。
-
Typora
一款Markdown编辑器。
-
IntelliJ IDEA
目前在业界被公认为最好的Java开发工具之一。
-
Java API文档
Java官方写好了注释,通过javadoc工具生成的文档。
Java 帮助文档首页:[Java 帮助文档](Java Platform, Standard Edition Documentation - Releases (oracle.com))
关于编程规范
编程规范是一种好习惯,虽然不是强制的,但大家都默认遵守。
编程规范也有利于团队合作,也利于找Bug,改Bug。
详细了解见《阿里巴巴Java开发手册》
注释
注释是写给我们自己看的,通过注释,我们能快速知道这段代码的作用。写注释是一个好习惯,注释不会出现在可执行程序中。
单行注释
以//
开始到该行结束的内容会被注释。
多行注释
以/*
开始到*/
结束的内容会被注释。
文档注释
以/**
开始到*/
结束,通常出现在类、方法、字段等的声明前面。这种注释可以被javadoc工具提取并生成API文档。
注释的插入
javadoc工具将从下面几项抽取信息:
- 模块
- 包
- 公共类与接口
- 公共的和受保护的字段
- 公共的和受保护的构造器及方法
文档标记
每个文档注释包含标记以及之后紧跟着的自由格式文本。标记以@开始。
在自由格式文本中可以使用HTML修饰符。
@param
@param
标记用于描述方法的参数。
@return
@return
标记用于描述方法的返回值。
@throws
@throws
标记用于描述方法可能抛出的异常。
@author
@author
标记用于指定作者信息。
@since
@since
标记用于使用该方法/类/接口的最低JDK版本。
@version
@version
标记用于指定代码或文档的版本号。
@see
@see
标记用于创建引用链接,指向其他相关的类、方法、文档或资源。
@see 完整包名.类名#方法()
{@link}
{@link}
作用与@see
相同,区别在于@see需要写在行首才会生效,{@link}可以写在任意地方。
{@link} 完整包名.类名#方法()
类注释
类注释必须放在import语句之后,class定义之前。
/**
* 类注释
* @author 作者名
* @since 17.0.11
* @version 1.0
*/
方法注释
每个方法注释必须放在所描述的方法之前。
/**
* 方法注释
* <p>
* 描述
* </p>
* @param parameterName 参数说明
* @return return 返回值
* @ throws Exception 异常的描述
*/
字段注释
只需要对公共字段(通常指的是静态常量)增加文档注释。
/**
* 描述字段
*/
使用javadoc工具生成API文档
-
cmd(命令提示符)
javadoc -d docDirectory -encoding UTF-8 -charset UTF-8 nameOfPackage
- -d 指定路径
- docDiectoy 生成文档的目录的路径
- -encoding 设置源码编码方式
- -charset 指定输出的字符编码格式
- nameOfPackage 生成文档的Java包的完整包名
需要注意的是:
找不到程序包中的源文件时,一般是包名键入错误、源文件不在预期的目录下。使用cd命令切换到与生成文档的Java包同级。
例如:javadoc -d ./doc -encoding UTF-8 -charset UTF-8 com.lin.www,就需要使用cd命令切换至与com文件夹同级。
-
IntelliJ IDEA
Tools -> Generate JavaDoc
Hello World
程序员对世界的呐喊!相当的有仪式感!
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
标识符
我们自己编写定义的名称称为标识符,如包名、类名、方法名、参数名、变量名等。
定义标识符的规则
- 所有标识符都应该以字母(A-Z或a-z)、美元符($)、下划线(_)开始。
- 首字符之后可以是字母(A-Z或者a-z)、美元符($)、下划线(_)或数字的任何字符组合。
- 不能使用关键字和保留关键字作为标识符。
需要注意的是:
- Java大小写敏感。
- 标识符不能包含空格。
- 尽管$是一个合法标识符,但不要在你的代码中使用这个字符。它一般用于Java编译器或其他工具生成名字。
定义标识符的建议
- 包名:所有字母使用小写。
- 类名:单词首字母大写。
- 变量名和方法名:首字母小写,多个单词组成时,除首个单词,其他单词首字母大写。
- 常量名:全部大写,多个单词,用下划线(_)连接。
关键字
Java编写定义的名称称为关键字。
受限关键字是指,在特定的地方是关键字,否则只是标识符。例如:var
,当它作为数据类型时,它才是关键字。
Java中的关键字
关键字 | 含义 | 类型 |
---|---|---|
byte | 8位整数类型 | 关键字 |
short | 16位整数类型 | 关键字 |
int | 32位整数类型 | 关键字 |
long | 64位长整数类型 | 关键字 |
char | Unicode字符类型 | 关键字 |
float | 单精度浮点数类型 | 关键字 |
double | 双精度浮点数类型 | 关键字 |
boolean | 布尔类型 | 关键字 |
true | 两个布尔值之一 | 字面量 |
false | 两个布尔值之一 | 字面量 |
null | 一个空引用 | 字面量 |
enum | 枚举类型 | 关键字 |
class | 定义一个类类型 | 关键字 |
switch | 一个选择语句或表达式 | 关键字 |
case | switch的一个分支 | 关键字 |
default | switch的默认子句,或者接口的默认方法 | 关键字 |
if | 一个条件语句 | 关键字 |
else | if语句的else子句 | 关键字 |
for | 一种循环类型 | 关键字 |
while | 一种循环 | 关键字 |
do | do/while循环最前面的语句 | 关键字 |
break | 跳出一个switch语句或循环 | 关键字 |
continue | 在循环末尾继续 | 关键字 |
return | 从一个方法返回 | 关键字 |
try | 捕获异常代码块 | 关键字 |
catch | try语句块中捕获异常的子句 | 关键字 |
finally | try语句块中总会执行的部分 | 关键字 |
throw | 抛出一个异常 | 关键字 |
throws | 一个方法可能抛出的异常 | 关键字 |
abstract | 抽象类或方法 | 关键字 |
assert | 用来查找内部程序错误 | 关键字 |
extends | 定义一个类的父亲,或者一个通配符的上界 | 关键字 |
final | 一个常量,或一个不能覆盖的类或方法 | 关键字 |
implements | 定义一个类实现的接口 | 关键字 |
import | 导入一个包 | 关键字 |
instanceof | 测试一个对象是否为一个类的实例 | 关键字 |
interface | 一种抽象类型,其中包含可以由类实现的方法 | 关键字 |
native | 由宿主系统实现的一个方法 | 关键字 |
new | 分配一个新对象或数组 | 关键字 |
non-sealed | 密封类型的一个子类型,可以构造它的任意子类型 | 关键字 |
package | 包含类的一个包 | 关键字 |
private | 这个特性只能由该类的方法访问 | 关键字 |
protected | 这个特性只能由该类、其子类以及同一个包中的其他类的方法访问 | 关键字 |
public | 这个特性可以由所有类的方法访问 | 关键字 |
static | 这个特性是类和接口特有的,而不属于类的实例 | 关键字 |
strictfp | 对浮点数计算使用严格的规则(过时) | 关键字 |
super | 超类对象或构造器,或一个通配符的下界 | 关键字 |
synchronized | 对线程而言具有原子性的方法或代码块 | 关键字 |
this | 当前类的一个方法或构造器的隐式参数 | 关键字 |
transient | 标记非永久的数据 | 关键字 |
void | 指示一个方法不返回任何值 | 关键字 |
volatile | 确保一个字段可以由多个线程一致地访问 | 关键字 |
const | 未使用 | 关键字 |
goto | 未使用 | 关键字 |
exports | 导出一个模块的包 | 受限关键字 |
module | 声明一个模块 | 受限关键字 |
open | 修改一个module声明 | 受限关键字 |
opens | 打开一个模块的包 | 受限关键字 |
permits | 引入密封类允许的子类型的一个列表 | 受限关键字 |
provides | 指示一个模块使用一个服务 | 受限关键字 |
record | 声明一个类,它有一组给定的final实例变量 | 受限关键字 |
sealed | 这个类型有一组受控制的直接子类型 | 受限关键字 |
to | exports或opens声明的一部分 | 受限关键字 |
transitive | 修饰一个requires声明 | 受限关键字 |
uses | 指示一个模块使用一个服务 | 受限关键字 |
var | 声明一个变量的类型是推导得出的 | 受限关键字 |
with | 在一个provides语句中定义服务类 | 受限关键字 |
yield | 生成switch表达式的值 | 受限关键字 |
数据类型
Java的数据类型分为两大类,基本类型和引用类型。
基本类型
Java语言提供了八种基本类型。六种数字类型(四种整数型,两种浮点型),一种字符类型,一种布尔类型。
整数类型
byte
- byte数据类型是8位、有符号,以二进制补码表示的整数
- 最小值是
-128(-2^7)
- 最大值是
127(2^7-1)
short:
- short 数据类型是 16 位、有符号的以二进制补码表示的整数
- 最小值是
-32768(-2^15)
- 最大值是
32767(2^15 - 1)
int:
- int 数据类型是32位、有符号的以二进制补码表示的整数
- 最小值是
-21_4748_3648(-2^31)
- 最大值是
21_4748_3647(2^31 - 1)
long:
- long 数据类型是 64 位、有符号的以二进制补码表示的整数
- 最小值是
-922_3372_0368_5477_5808(-2^63)
- 最大值是
922_3372_0368_5477_5807(2^63 -1)
需要注意的是:
整数类型默认是int,使用long类型需要使用L标记
long num = 123580L;
数字以0B
开头的表示二进制,0
开头的表示八进制,0X
开头的表示十六进制
浮点类型
float:
- float 数据类型是单精度、32位、符合IEEE 754标准的浮点数
double:
- double 数据类型是双精度、64 位、符合 IEEE 754 标准的浮点数
需要注意的是:
浮点类型默认是double,使用float类型需要使用F标记
float num = 3.1415926F;
浮点数数值不适用于无法接受舍入误差的金融计算。
例如,命令System.out.println(2.0-1.1);将打印出0.89999999,而不是我们期望的0.9。这种舍入误差主要原因是浮点数值采用二进制表示,无法精确表示分数1/10。这就好比十进制无法精确表示分数1/3一样。如果需要精确的数值计算,不允许有舍入误差,则应该使用BigDecimal
类。
字符类型
char:
- char 类型是一个单一的 16 位 Unicode 字符
- 最小值是
\u0000
- 最大值是
\uffff
需要注意的是:
字符类型有且只有一个字符
布尔类型
boolean:
- boolean数据类型表示一位的信息
- 只有两个取值:
true
和false
需要注意的是:
整数类型默认值为0,浮点类型默认值为0.0,字符类型默认值为’\u0000’,布尔类型默认值为false,引用类型默认值为null
基本类型间的数据类型转换
运算中,不同类型的数据会先转成同一类型,再进行运算。
自动类型转换
Java中类型等级从低到高分别为(byte,short,char)->int->long->float->double
低级转高级就会自动转换。
强制类型转换
高级转低级需要强制转换。
语法格式:(type)value
// int 转换成 char
int a = 10;
char c = (char)a;
// double 转换成 int
double d = 5.2;
int b = (int)d;
// JDK7特性,数字间可以使用下划线分隔。下划线只是让人更易读,Java编译器会去除这些下划线。
int a = 10_0000_0000;
int b = 20;
long c = a*b;
/*
我们理想的情况: c = 200_0000_0000;
结果: 溢出,我们无法赋予超过该类型表示范围的值
因为在a*b的运算中是两个int类型进行运算,运算完再将结果转换成long
a*b很明显超过了int的范围。为保证达到我们理想的情况,我们应该将其更正为
*/
long c = (long)a*b;
需要注意的是:
-
byte,short,char之间需要强制类型转换。
-
不能对布尔值进行类型转换。
-
强制类型转换可能会损失一些信息。
int a = 200;
byte b = (byte) a;
int类型a占32位,byte类型b占8位,将一个大于127的int值强制转换为byte类型时,会截断高位,保留低位的8位二进制作为byte的值。
a = 00000000000000000000000011001000
在截取低8位时,最高位被解释为负数的符号位。
b = 11001000
Java中的整数类型采用补码表示,11001000 >取反> 10110111 >加一> 10111000。打印b的值会得到-56
System.out.println(b);
-
int->float或long->float或long->double可能会出现精度问题。
引用类型
-
数组
-
类
-
接口
引用类型间的数据类型转换
引用类型之间的转换分为向上转型(upcasting)和向下转型(downcasting)两种。
向上转换
向上转型是指将一个子类类型的实例赋值给一个父类类型的引用变量。这种转换是安全的,不需要显式的类型转换,因为子类对象具有父类对象的所有属性和方法(不包括private修饰的)。
class Animal {}
class Dog extends Animal {}
Dog dog = new Dog();
// 向上转型,不需要显式类型转换
Animal animal = dog;
需要注意的是:
向上转型时,子类对象被视为父类对象。这意味着只能访问父类中定义的字段和方法,而无法直接访问子类中独有的字段和方法。因为编译器只知道引用变量的类型是父类,而不知道这个引用变量实际上指向的是子类的对象。
如果需要访问子类独有的字段和方法,可以考虑使用向下转型,将父类类型的引用变量强制转换为子类类型,从而可以访问子类特有的字段和方法。在进行向下转型之前,最好使用 instanceof 运算符来检查引用变量是否确实指向了子类的对象,以避免可能出现的类型转换异常。
向下转换
向下转型是将一个父类类型的引用指向一个子类对象,转换为子类类型的引用指向子类对象。这种转换是不安全的,需要进行显式的类型转换,并且在运行时可能会抛出ClassCastException异常。
Animal animal = new Dog();
// 向下转型,需要显式类型转换
Dog dog = (Dog) animal;
变量
变量就是可以变化的量。
Java是一种强类型语言,每个变量都必须声明其类型。不能使用未初始化的变量。
// 语法:数据类型 变量名 = 值;
type varName = value;
// 可以使用逗号分隔,来声明多个同类型变量
type varName1 = value, varName2 = value;
根据变量声明位置的不同可以分为局部变量和成员变量。
-
局部变量
定义在方法里的变量被成为局部变量。随着方法的调用而创建,随着方法的消失而消失。
-
成员变量(实例变量)
定义在类中,方法外的变量被称为成员变量。成员变量所属于对象,随着对象创建而创建,随着对象消失而消失。
public class 类名 {
// 成员变量
int b;
public static void main(String[] args) {
// 局部变量
int i = 10;
}
}
块作用域
Java块作用域由{}界定,块确定了变量的作用域。在一对{}里,变量的作用域由"{“开始,”}"结束。
public class 类名 {
public static void main(String[] args) {
int a = 1;// 变量a作用域开始
if(XXX) {
int b = 2;// 变量b作用域开始
}// 变量b作用域结束
}// 变量a作用域结束
}
static
static
关键字用于声明静态成员。所有类的实例共享唯一的静态成员,而不是每个实例都有自己的一份拷贝。静态成员使用类名访问。
静态变量
由static关键字修饰的成员变量被称为静态变量。
static int N = 10;
静态常量
由static关键字修饰的成员常量被称为静态常量。
static final int N = 10;
静态方法
由static关键字修饰的成员方法被称为静态方法。
public static void study() {
}
导入静态成员
可以使用静态导入来直接导入类的静态成员(包括静态字段和静态方法),使得在代码中可以直接通过成员的名称来使用,而无需使用类名来限定。
导入Math类中的静态方法:import static java.lang.Math.abs;
导入Math类中的静态字段:import static java.lang.Math.PI;
静态代码块
加载类的时候执行静态代码块,如果有多个静态代码块,按照顺序结构执行。将静态代码块置于main方法之上,静态代码块将先于main方法执行。
static {
}
成员与静态成员的区别
静态成员所属与类,随着类的加载而加载,随着类的消失而消失。
成员所属于对象,随着对象创建而创建,随着对象消失而消失。
类的初始化是类加载完毕后,对类进行初始化的过程。在类初始化时,以顺序结构执行静态代码块和静态变量赋值语句(先初始化父类,父类初始化完毕后初始化子类)。
类的每个实例共享一份静态成员,类的每个实例都有一份独有的成员。
运算符
运算符根据操作数分类:
一元运算符:一元运算符有1个操作数。例如自增自减运算符。
二元运算符:二元运算符有2个操作数。例如算数运算符。
三元运算符:三元运算符有3个操作数。例如条件运算符。
操作数:
操作数是指参与运算的数据或值,它规定了指令中进行数字运算的量 。表达式由操作数与操作符(运算符)的组合。
算术运算符
算数运算符 | 含义 | 描述 |
---|---|---|
+ | 加法 | 相加运算符两侧的值 |
- | 减法 | 左操作数减去右操作数 |
* | 乘法 | 相乘运算符两侧的值 |
/ | 除法 | 左操作数除以右操作数,取商 |
% | 取余 | 左操作数除以右操作数,取余数 |
需要注意的是:
当"+“两侧操作数有一个String类型的字符串,那么后面的”+"就不再是相加,而会变成字符拼接。
int a = 1;
int b = 2;
System.out.println("9" + a + b);// 输出结果: 912 类型String 过程--> 【"9" 与 a 拼接】,【"1" 与 b 拼接】
System.out.println(a + b + "");// 输出结果: 39 类型String 过程--> 【a 与 b 进行加法算数运算】,【3 与 "9" 拼接】
自增自减运算符
自增自减运算符 | 含义 | 描述 |
---|---|---|
++ | 自增 | 操作数的值加1 |
-- | 自减 | 操作数的值减1 |
自增自减运算符有前缀和后缀两种形式:
int i = 0;
++i, --i, i++, i--;// i加一, i减一, i加一, i减一;
在单独使用时,前缀形式与后缀形式没有区别,可以将其理解 i = i + 1,i = i - 1;
在表达式中使用时,可以将自增自减运算理解为两部分(更新和使用)
当自增自减运算符为前缀形式时,先更新再使用;当自增自减运算符为后缀形式时,先使用后更新。
int a = i++;// 后缀形式。i先被使用,将值赋值给a,再自增更新加一。此时,a = 0,i = 1;
System.out.println(a);// 打印结果为0
int b = --i;// 前缀形式。i先自减更新减一,再被使用,将值赋值给b。此时,b = 0,i = 0;
System.out.println(b);// 打印结果为0,
位运算符
位运算符 | 描述 |
---|---|
&(与) | 如果相对应位都是1,则结果为1,否则为0 |
|(或) | 如果相对应位都是0,则结果为0,否则为1 |
^(异或) | 如果相对应位值相同,则结果为0,否则为1 |
~(取反) | 按位取反操作数的每一位,即0变成1,1变成0 |
<< | 左操作数按位左移右操作数指定的位数 |
>> | 左操作数按位右移右操作数指定的位数 |
>>> | 做操作符按位右移右操作数指定的位数,移动得到的空位以0填充 |
赋值运算符
= | 将右操作数的值赋给左侧操作数 |
---|
赋值运算符扩展: +=,-=,*=,/=,%=,&=,|=,^=,<<=,>>=,>>>=
可以在赋值中使用二元运算符,为此有一种简写形式。例如
- x += 4; 等价于 x = x + 4;
- x >>= 4; 等价于 x = x >> 4;
关系运算符
关系运算符 | 描述 |
---|---|
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为true |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为true |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为true |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为true |
== | 检查两个操作数的值是否相等,如果相等则条件为true |
!= | 检查两个操作数的值是否相等,如果相等则条件为false |
instanceof | 测试左侧对象是否为instanceof右侧类的实例或实现了右侧的接口,如果是则条件为true |
逻辑运算符
逻辑运算符 | 描述 |
---|---|
&& | 当两个操作数都为true,则条件为true。当左操作数为false,右操作数不执行判断。 |
|| | 当两个操作数有一个为true,则条件为true。当左操作数为true,右操作数不执行判断。 |
! | 当操作数为true,则条件为false;当操作数为false,则条件为true |
条件运算符
Java提供了?:
运算符,可以根据一个布尔表达式的结果选择计算表达式的值。
【格式】:布尔表达式 ? 表达式1 : 表达式2
如果布尔表达式为true,就计算第一个表达式的值返回,否则计算第二个表达式的值返回。
运算符优先级
优先级:决定了表达式中运算执行的先后顺序。例如数学中先乘除后加减,乘除的优先级就高于除法。
结合性:决定了表达式中相同级别运算符的先后顺序。例如数学中加减是相同优先级,它的结合性就是从左向右,依次运算。
优先级 | 运算符 | 结合性 |
---|---|---|
1 | [] . () | 从左向右 |
2 | ! ~ ++ -- | 从右向左 |
3 | * / % | 从左向右 |
4 | + - | 从左向右 |
5 | << >> >>> | 从左向右 |
6 | < <= > >= instanceof | 从左向右 |
7 | == != | 从左向右 |
8 | & | 从左向右 |
9 | ^ | 从左向右 |
10 | | | 从左向右 |
11 | && | 从左向右 |
12 | || | 从左向右 |
13 | ?: | 从右向左 |
14 | = += -= *= /= %= &= |= ^= <<= >>= >>>= | 从右向左 |
15 | , | 从右向左 |
顺序结构
Java的基本结构就是顺序结构,除非特别指明,否则程序自顶向下依次执行。
格式化输出
格式化输出是指将数据按照指定的格式输出到标准输出设备(如屏幕)或文件中。格式化输出分为格式说明符和要输出的数据两部分。
System.out.printf("%d", 123580);
格式化输出的格式说明符
格式说明符 | 描述 |
---|---|
%d | 带符号的十进制整数 |
%o | 不带符号的八进制数 |
%x | 不带符号的十六进制数,使用小写字母表示 |
%X | 不带符号的十六进制数,使用大写字母表示 |
%f | 带小数点的十进制浮点数 |
%e | 带指数的浮点数,使用小写字母表示 |
%E | 带指数的浮点数,使用大写字母表示 |
%g | 根据数值大小自动选择%f或%e格式 |
%G | 根据数值大小自动选择%f或%E格式 |
%a | 十六进制浮点数,使用小写字母表示 |
%A | 十六进制浮点数,使用大写字母表示 |
%c | 单个字符 |
%C | 大写的单个字符 |
%s | 字符串 |
%S | 大写的字符串 |
%b | true或false |
%B | TRUE或FALSE |
%h | 散列码 十六进制,使用小写字母表示 |
%H | 散列码 十六进制,使用大写字母表示 |
%n | 与平台有关的行分隔符 |
%% | 百分号 |
%t(%T) | 日期和时间格式化 |
%tc
:完整的日期时间
%tF
:年月日
%tD
:月日年
%tT
:24时制的时分秒
%tR
:24时制的时分
%tj
:一年中的第几天
%tY
:四位数的年份
%ty
:两位数的年份
%tm
:两位数的月份
%td
:两位数的日期
%te
:一位或两位数的日期
%tH
:24小时制的两位小时数
%tk
:24小时制的一位或两位小时数
%tI
:12小时制的两位小时数
%tl
:12小时制的一位或两位小时数
%tB
:本地化的月份全名
%tb
:本地化的月份缩写
%tA
:本地化的星期全名
%ta
:本地化的星期缩写
%tM
:分钟数
%tS
:秒数
%tp
:上午或下午
%tZ
:时区
散列码
散列码是指通过散列函数对数据进行转换后得到的固定长度的值。
散列码的作用
- **数据唯一标识:**散列码将任意长度的数据映射成固定长度的值,这使得散列码非常适合作用于数据的唯一标识。
- **验证数据的完整性:**发送方使用散列函数对消息进行散列运算,将得到的散列码附加到消息中。接收方使用相同的散列函数对接收到的消息进行散列运算,并将结果与发送方提供的散列码进行比较,验证消息是否在传输过程中被篡改。
- **密码哈希:**密码通常不会以明文形式存储在数据库中,而是以散列码的形式存储。当用户登录时,系统会将用户输入的密码进行散列运算,并将结果与数据库中存储的散列码进行比较,以验证用户身份。
- **加密:**散列码也可用在加密算法中,用于保证数据的安全性和完整性。
格式化输出的修饰符
格式化输出中可以使用修饰符用来进一步控制格式化输出的样式。
宽度修饰符
在%后面添加数字
,指定输出字段的最小宽度。例如:%5d
精度修饰符
在%后面添加.数字
,指定浮点数小数部分的位数。例如:%.2f
标志修饰符
在%后面添加标志
,控制格式化输出的格式。
标志 | 描述 |
---|---|
+ | 输出正数的符号 |
空格 | 在正数前面增加一个空格 |
( | 将负数包裹在括号内 |
#(对应%f格式) | 总是包含一个小数点。小数部分的位数为0时,仍包含小数点 |
#(对应%o格式) | 添加前缀0 |
#(对应%x格式) | 添加前缀0x |
0 | 在字段宽度不足时使用0填充 |
- | 字段左对其(默认右对齐) |
, | 为整数部分增加分隔符 |
[索引]$ | 指定要格式化的参数索引(索引从1开始) |
< | 格式化前面指定的同一个值 |
每一个以%开头的格式说明符代表一个占位符,与后面的参数一一对应。但带有$和<标志的格式说明符不算在内。
System.out.printf(“%d %d %2$d %<d %d”, 1, 2, 3); // 输出结果为:1 2 2 2 3
组合使用时为避免冲突,应该按$、0、指定宽度的顺序进行修饰。例如:%1$05d
选择结构
if
if语句由if
、else if
、else
三部分组成,其中else if
在一个if语句中可以有多个。if语句通常以if、if-else、if-else if、if-else if-else四种方式单独或嵌套组合使用。
if语句的执行顺序
-
判断if语句中的布尔表达式:
- 如果值为true,则执行if代码块中的内容,并跳过余下的所有else if和else子句,整个if语句结束
- 如果值为false,则查看该if语句中是否存在else if、else子句,如果不存在,整个if语句结束
-
如果存在else if子句:
-
判断else if子句中的布尔表达式
-
如果值为true,则执行该else if代码块中的内容,并跳过余下的所有else if和else子句,整个if语句结束
-
如果值为false,则查看该if语句中是否还存在else if、else子句,如果不存在,整个if语句结束
-
-
执行else子句中的代码块:
- if、else if中的所有布尔表达式都为false,且存在else子句,则执行else表达式中的代码块,整个if语句结束
- if、else if中的所有布尔表达式都为false,但不存在else 子句,整个if语句结束
public class 类名 {
public static void main(String[] args) {
if(布尔表达式) {
// 代码块
}else if (布尔表达式) {
// 代码块
}else if (布尔表达式){
// 代码块
...
}else {
// 代码块
}
}
}
关于if语句的例子
判断并输出数字number的范围。以下将执行else代码块中的语句。
public class Main {
public static void main(String[] args) {
int number = 5;
if (number < 5) {
System.out.println("数字number小于5");
}else if (number > 5) {
System.out.println("数字number大于5");
}else {
System.out.println("数字number等于5");
}
}
}
switch
处理同一个表达式的多个选择时,相比较if,使用switch语句会更容易。
switch语句由switch
、case
、default
三部分组成,其中case
可以有多个分支。
case标签可以是:
- 类型为char、byte、short、int的常量表达式
- 枚举常量
- 字符串字面量
case中的标签也可以拥有多个,用逗号分隔
switch语句的执行顺序
- 计算switch语句中表达式的值。
- 将该值与每个case标签的数据进行匹配。
- 如果匹配成功,则从匹配的case分支开始执行相应的语句,并继续执行下面的分支(这种现象被称为"case穿透"或直通),直到遇到break关键字或者switch语句结束。
- 如果没有匹配成功且存在default分支,则执行default分支下的语句,并继续执行下面的分支,直到遇到break关键字或者switch语句结束。
public class Main {
public static void main(String[] args) {
switch(表达式) {
// 可添加括号增加可读性
case 标签1: {
// 语句
break; // 可选
}
case 标签2, 标签3:
// 语句
case 标签4:
// 语句
...
default:
// 语句
}
}
}
如果不希望switch中出现“直通”的情况,需要为每一个case中添加一个break来跳出switch
关于switch语句的例子
根据数字判断星期几。以下dayString将被赋值"Weekend"
public class Main {
public static void main(String[] args) {
int day = 6;
String dayString;
switch (day) {
case 1:
dayString = "Monday";
break;
case 2:
dayString = "Tuesday";
break;
case 3:
dayString = "Wednesday";
break;
case 4:
dayString = "Thursday";
break;
case 5:
dayString = "Friday";
break;
default:
dayString = "Weekend";
}
System.out.println("今天是:" + dayString);
}
}
Java14的switch
在Java14中引入了switch表达式和一些对switch语句简化的形式。
将:
换成->
可省略break
public class Main {
public static void main(String[] args) {
int day = 6;
String dayString;
switch (day) {
case 1 -> dayString = "Monday";
case 2 -> dayString = "Tuesday";
case 3 -> dayString = "Wednesday";
case 4 -> dayString = "Thursday";
case 5 -> dayString = "Friday";
default -> dayString = "Saturday";
}
System.out.println("今天是:" + dayString);
}
}
switch表达式
与switch语句不同的是,switch表达式的每个分支必须生成一个值。这也意味着在switch表达式中不允许使用break或return关键字。
yield
yield关键字与break类似会终止switch,与之不同的是yield会生成一个表达式的值
public class Test {
public static void main(String[] args) {
int day = 6;
String dayString = switch (day) {
case 1:
yield "Monday";
case 2:
yield "Tuesday";
case 3:
yield "Wednesday";
case 4:
yield "Thursday";
case 5:
yield "Friday";
default:
yield "Saturday";
};
System.out.println("今天是:" + dayString);
}
}
当case只是返回一个值时,也可以使用->
简化
public class Main {
public static void main(String[] args) {
int day = 6;
String dayString = switch (day) {
case 1 -> {
System.out.println("今天是讨厌的周一");
yield "Monday";
}
case 2 -> "Tuesday";
case 3 -> "Wednesday";
case 4 -> "Thursday";
case 5 -> "Friday";
default -> "Saturday";
};
System.out.println("今天是:" + dayString);
}
}
循环结构
确定性循环
for
for循环的执行步骤
- 执行[初始化]语句
- 计算布尔表达式的值,如果值为true,执行代码块中的语句;如果值为false,for循环结束。
- 执行[更新]语句
在整个for循环中,步骤1只执行一次,之后不断重复执行步骤2、步骤3,直到步骤2中的布尔表达式的值为false。
public class 类名 {
public static void main(String[] args) {
for(初始化;布尔表达式;更新) {
// 代码块
}
}
}
for each
Java 5 引入了一种增强的for循环,可以用来依次处理数组(或着其他元素集合)中的每个元素,而不必考虑指定的索引值。
for each将依次为变量(variable)赋予collection中的每一个元素,并执行代码块中的语句。其中,collection表达式必须是一个数组或是一个实现了Iterable接口的类对象。
public class 类名 {
public static void main(String[] args) {
for([数据类型] variable:collection) {
// 代码块
}
}
}
非确定循环
while
while循环的执行步骤
- 计算while中的布尔表达式的值,如果值为true,则执行代码块中的语句;如果值为false,while循环结束。
在整个while循环中,不断重复执行步骤1,直到步骤1中的布尔表达式的值为false
public class 类名 {
public static void main(String[] args) {
while(布尔表达式) {
// 代码块
}
}
}
do-while
do-while的执行步骤
- 执行do代码块中的语句
- 计算while中的布尔表达式的值,如果值为true,则执行步骤1;如果值为false,do-while循环结束。
在整个do-while循环中,步骤1执行一次后,不断重复执行步骤2,直到步骤2中的布尔表达式的值为false
public class 类名 {
public static void main(String[] args) {
do {
// 代码块
}while(布尔表达式);
}
}
带标签的continue、break
我们已经了解break关键字是跳出switch、循环,return是从一个方法中返回,continue是跳到循环中代码块的末尾继续。而带标签的 continue 语句允许你在嵌套循环中跳过当前循环,并继续执行指定标签的外部循环。
带标签的 break 语句允许你在嵌套循环中立即退出指定标签的循环。
public class 类名 {
public static void main(String[] args) {
// 打印100-150之间的质数
标签:for(int i = 100;i<150;i++) {
for(int j = 2;j<i/2;i++) {
if(i % j == 0) {
continue 标签;
}
}
}
// 遍历二维数组,如果数组中有6,则输出第一个6出现的下标
int[][] arr = {{1,2,3},{4,5,6},{7,8,9}};
标签:for(int i = 0; i < arr.length; i++) {
for(int j = 0; j < arr[i].length; j++) {
if(arr[i][j] == 6) {
System.out.printf("index: [%d][%d]", i, j);
break 标签;
}
}
}
}
}
数组
数组是一种数据结构,用来存储相同类型数据的集合,其中每个数据被称为元素,通过一个整型索引(index,下标)可以访问数组中的每一个元素。索引从0开始。
数组的声明 & 创建
public class 类名 {
public static void main(String[] args) {
// 声明语法
数据类型[] 数组名;
// 创建语法
数组名[] = new 数据类型[数组长度];
// 此外,Java提供了一种创建数组对象并同时提供初始值的简写形式
数据类型[] 数组名 = {元素1,元素2...};
}
}
需要注意的是:
- 一旦数组被创建就不能再改变它的长度
- 数组可以声明任意类型(引用类型、基本类型),但数组中的元素必须与声明类型相同
访问数组中的元素
public class 类名 {
public static void main(String[] args) {
// 创建一个String类型 长度10 的数组arrs
String[] arrs = new String[10];
// 通过下标,分别访问数组第一个元素、第二个元素、第三个元素,并赋给对应的值
arrs[0] = "hello";
arrs[1] = "World";
arrs[2] = "!";
// 通过for循环输出数组前3个元素
for(int i = 0; i<3; i++) {
System.out.println(arrs[i]);
}
// 创建一个int类型 长度5 的数组nums,并初始化每个元素对应的值
int[] nums = {1,2,3,4,5};
// 通过for each循环输出数组所有元素
for(int element : nums) {
System.out.println(element);
}
}
}
- 获取数组长度:数组名.lenght
需要注意的是:
将数组变量arr拷贝给数组变量temp时,temp得到的是arr的地址而非arr的值,它们将引用同一个数组的值。在通常情况下,只有在使用new关键字创建对象时,虚拟机才会分配空间。
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8};
int[] temp = arr;
temp[5] = 9;
System.out.println(Arrays.toString(arr));
System.out.println(Arrays.toString(temp));
}
}
多维数组
public class 类名 {
public static void main(String[] args) {
// 多维数组的创建
数据类型[][]...[] 数组名 = new 数据类型[数组长度][数组长度]...[数组长度];
// 其中二维数组最简单,相当于数组里套用了一个数组
int[][] arrs = new int[2][3];
// 访问二维数组中的元素
arrs[0][1] = 1; // 为二维数组中,第一个数组中的第二个元素赋值1
arrs[1][1] = 2; // 为二维数组中,第二个数组中的第二个元素赋值2
// 创建数组对象,并提供初始值的简写形式
int[][] nums = {{1,2,3},{1,2,3}};
// 遍历二维数组
for(int i = 0; i<nums.lenght; i++) {
for(int j = 0; j<nums[i].lenght; j++) {
System.out.println(nums[i][j]);
}
}
}
}
不规则数组
public class Main {
public static void main(String[] args) {
final int NMAX = 5;
// 创建不规则数组,即每一行有不同的长度
int[][] odds = new int[NMAX][];
for (int n = 0; n < NMAX; n++) {
odds[n] = new int[n + 1];
}
// 计算杨辉三角每一个位置上的值
for (int n = 0; n < odds.length; n++) {
for (int k = 0; k < odds[n].length; k++) {
int lotteryOdds = 1;
for (int i = 1; i <= k; i++) {
lotteryOdds = lotteryOdds * (n - i + 1) / i;
}
odds[n][k] = lotteryOdds;
}
}
// 打印杨辉三角
for (int[] row : odds) {
for (int odd : row) {
System.out.printf("%4d", odd);
}
System.out.println();
}
}
}
稀疏数组
稀疏数组(Sparse Array)是一种数据结构,用于表示大部分元素为相同值的数组。它通过只存储非默认值的元素及其位置来节省空间,从而在大多数元素为默认值时显著减少内存消耗。
public class Main {
public static void main(String[] args) {
/*
以默认值为0举例。
使用二维数组存储:
0 0 0 0 6
0 0 0 0 0
0 5 0 0 0
0 0 0 0 0
0 0 0 0 0
使用稀疏数组存储:
5 5 2
0 4 6
2 1 5
稀疏数组[0][0]用来存储行,[0][1]用来存储列,[0][2]用来存储有多少个有效值。
*/
// 二维数组
int[][] array = {{0, 0, 0, 0, 6}, {0, 0, 0, 0, 0}, {0, 5, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}};
// 打印二维数组
for (int[] arr : array) {
for (int integer : arr) {
System.out.print(integer + " ");
}
System.out.println();
}
System.out.println("==================分界线==================");
// 获取二维数组的行和列
int row = array.length;
int col = array[0].length;
// 获取二维数组元素非默认值的个数
int count = 0;
for (int[] arr : array) {
for (int integer : arr) {
if (integer != 0) {
count++;
}
}
}
// 创建稀疏数组
int[][] sparseArray = new int[count + 1][3];
// 分别存储二维数组的行、列、有效元素的个数
sparseArray[0][0] = row;
sparseArray[0][1] = col;
sparseArray[0][2] = count;
// 存储非默认值的值和位置
count = 1;
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
if (array[i][j] != 0) {
sparseArray[count][0] = i;
sparseArray[count][1] = j;
sparseArray[count][2] = array[i][j];
count++;
}
}
}
// 打印稀疏数组
for (int[] arr : sparseArray) {
for (int integer : arr) {
System.out.print(integer + " ");
}
System.out.println();
}
System.out.println("==================分界线==================");
// 稀疏数组还原为二维数组
int[][] temporaryArray = new int[sparseArray[0][0]][sparseArray[0][1]];
for(int i = 1; i < sparseArray.length; i++) {
temporaryArray[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
}
// 打印还原后的二维数组
for (int[] arr : temporaryArray) {
for (int integer : arr) {
System.out.print(integer + " ");
}
System.out.println();
}
}
}
方法
方法的声明
public class 类名 {
public static void main(String[] args) {
// 实参
int number = sum(3,5);
}
// 形式参数
修饰符 返回值类型 方法名(参数类型 参数名...) {
...
// 定义该方法的功能
...
return 返回值;
}
// 定义了一个返回number1 number2之和的方法
public static int sum(int number1,int number2) {
int number3 = number1 + number2;
return number3;
}
}
**返回值类型:**基本类型、引用类型、void(定义方法无返回值)。方法可能有返回值,返回值通过return关键字返回。
**形参 & 实参:**形式参数是指被调方法中的参数,表明方法参数个数和类型;实际参数是指主调函数赋值给被调函数的具体值。
方法的调用
当方法的返回类型为void,该方法通常会被当做一条语句;当方法的返回类型不为void,该方法通常会被当做一个值。
-
调用静态方法方法
[类名].[方法名]
-
调用成员方法
[对象名].[方法名]
需要注意的是:
-
在类中调用当前类中的静态方法,可以省略类名
-
可以使用类的实例(对象)调用类中的静态方法,但为了避免混淆,建议使用类名调用
-
在 Java 中,参数传递是按值传递(pass by value)的。这意味着当你将一个变量传递给一个方法时,实际上传递的是该变量的值的副本,而不是该变量的引用。因此,对于基本数据类型,方法内部对参数的修改不会影响原始变量的值。而对于引用类型,传递的是对象引用的副本,因此方法内部对对象属性的修改会影响原始对象。
向main方法传参
通过命令行终端向 main
方法传递参数。这些参数会作为程序启动时的命令行参数传递给 main
方法,可以在程序运行时使用这些参数。
main方法的签名通常为main(String[] args)
public class Main {
// 定义方法:打印数组array中的每一个元素
public static void printArray(String[] array) {
// 遍历输出数组中的元素
for(int i = 0; i < array.length; i++) {
System.out.println("args[" + i + "]:" + array[i]);
}
}
public static void main(String[] args) {
printArray(args);
}
}
在命令行中运行该程序时,在命令后面添加参数,这些参数会被传递给 main
方法。
# 编译java文件
javac -encoding UTF-8 ./Main.java
# 执行class文件,通过命令行传递参数(this is file)
java ./Main this is file
递归
我们能在A方法中调用B方法,而递归就是在A方法中调用A方法,自己调用自己。
递归思想方便于人理解,但解决一个问题是否使用递归思想,需要好好斟酌。
public class Demo {
// 定义方法:求n的阶乘
public static int factorial(int n) {
if(n < 2) {
return 1;
}else {
return n * factorial(n - 1);
}
}
public static void main(String[] args) {
int result = factorial(3);
System.out.println(result);
}
}
// 递归求3的阶乘思想
// factorial(3) = 3 * factorial(2)
// factorial(2) = 2 * factorial(1)
// factorial(1) = 1
// factorial(3) = 3 * 2 * 1 = 6
需要注意的是:
递归需要有一个出口,例如:当 n < 2 时返回 1 ,结束递归。如果递归没有出口,会出现栈溢出,这是一种严重的错误。
栈:将栈想象成一个盛水的木桶,将方法比作一杯水,每执行一个方法,就往木桶里倒一杯水,方法结束将水打走。递归不断地调用自身,方法短时间不能结束,水不断往上积累…所以递归一定需要一个出口来结束递归。
方法的重载
多个方法,相同的方法名,不同的参数,这种现象被称为方法的重载。
需要注意的是:
使用方法的签名完整的描述一个方法,方法的签名包括方法的名称和参数列表(参数的数量、类型和顺序)。
返回类型不是方法签名的一部分。也就是说可以有方法签名不同,返回类型不同的方法;不能有方法签名相同,返回类型不同的方法。
可变参数
可变参数(variable arguments)允许方法接受数量可变的参数。
可变参数的声明增加了三个点号(…),在参数类型后面,例如 typeName...parameterName
。
可变参数只能作为方法的最后一个参数,一个方法最多只能有一个可变参数。
可变参数本质是数组。
public class Main {
public static void intMax(int...n) {
int result = n[0];
for(int i = 1; i < n.length; i++) {
if(result < n[i]) {
result = n[i];
}
}
System.out.println("这当中最大的数是:" + result);
}
public static void main(String[] args) {
intMax(1,5,3,7,8,2,9);
}
}
包机制
包机制是Java中的一个核心概念,用于组织和管理代码结构。通过包机制,可以避免命名冲突、实现代码模块化、增强代码的可维护性和可重用性。
包的结构
包是一组相关的类和接口的集合。这些类和接口通常位于文件系统中的同一目录。
包的定义
包通常用一个公司或组织的反向域名来命名。如com.lin.www
。
包的声明
每个 Java 类文件都可以通过在文件开头使用 package
关键字来声明所属的包。如package com.lin.www
。
包的导入
一个类可以使用当前包中的所有类,以及其他包中的公共类(public class
)。
访问其他包中的公共类:
-
使用完全限定名(包名后面跟着类名)
java.util.Scanner scanner = new java.util.Scanner(System.in);
-
import语句
import语句位于源文件的顶部(但位于package语句的后面)使用import语句
import java.util.*
,导入java.util
包中的所有类。使用import语句导入某个类后,使用对应包中的类时就不必写出类的全名了。
访问权限修饰符
访问修饰符用于控制类、方法、变量等的访问权限。Java中有四种访问修饰符:private、默认不写、protected和public。
- private:
- 只能在声明它们的类的内部访问。
- 不可被同一包中其他类或不同包中的任何类访问。
- 默认不写:
- 包私有,对同一包中的其他类可见,对不同包中的类不可见。
- 可被同一包中其他类访问,但对于不同包中的类不可见。
- protected:
- 对同一包中的其他类和任何继承该类的子类可见。
- 可被同一包中其他类和任何继承该类的子类访问。
- public:
- 对于任何类都是可见的,无论它们是否在同一包中。
- 可被任何类访问。
修饰符 | 同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 |
---|---|---|---|---|
private | 可见 | |||
默认不写 | 可见 | 可见 | ||
protected | 可见 | 可见 | 可见 | |
public | 可见 | 可见 | 可见 | 可见 |
面向对象基础
“万物皆对象,没有对象怎么办?new一个就好啦”。
概念
面向对象思想
对于描述复杂的事物,从宏观上把握,从整体上合理分析,我们需要使用面向对象的思路。但是,具体到微观操作,仍然需要面向过程的思路去处理。
面向对象编程的本质
以类的形式组织代码,以对象的方式封装数据。
关于main方法
一般来讲,一个程序中应该只有一个main方法,是整个程序的入口,常被称为主方法。拥有main方法的类被称为主类,除主类外的其他的类有各种各样的功能,但它们的设计目的都是围绕着主类的。
静态成员和非静态成员
在非静态方法中,我们可以直接使用this
关键字调用该对象的其他非静态方法。this所指当前对象。
public class Main {
public void method() {
System.out.println("Hello World");
}
public void printHelloWorld() {
this.method();
}
}
在静态方法中,我们需要实例化类,使用的对象调用非静态方法。这是因为,在程序加载类时,静态成员就会被初始化,使得我们可以在任意合法的地方通过类名调用静态成员;而非静态成员,只有当对象被创建时,非静态成员才会分配内存空间。
类 & 对象
设计类
类有多个成员组成。成员通常指的是类内部的构成要素,包括字段、方法、构造器、内部类等。
// 使用class关键字声明Student类
public class Student {
// 字段
private String name;
private int age;
// 构造器
public Student() {
}
// 方法
public void study() {
System.out.printf(this.name + "在学习~");
}
}
需要注意的是:
- 一个源文件中只能有一个 public 类
- 一个源文件可以有多个非 public 类
- 源文件的名称应该和 public 类的类名保持一致。
- 如果类中存在package语句或import语句,那么类的结构自顶向下应该是:package语句、import语句、类。
- import 语句和 package 语句对源文件中定义的所有类都有效。
类的实例化
类的实例化也叫创建关于这个类的对象。
public class Main {
public static void main(String[] args) {
// 实例化:通过new关键字创建关于Student类的对象
Student student = new Student();
// 还记得引用类型吗,可以这么理解
// 数据类型 类型名 = new 构造器
}
}
使用new关键字创建对象,除了会分配内存之外,还会对该对象进行默认的初始化,以及对类中构造器的调用。
访问实例变量和方法
// 实例化对象
Student student = new Student();
// 访问类中的变量
student.name = "陈你好";
// 访问类中的方法
student.study();
类与对象的关系
类是抽象的,当我们使用类描述一个学生时,学生类有什么属性?学生可能有姓名,年龄等。学生类可能有什么活动?学生大部分时间可能都在学习。
对象是类的具体实现,例如学生1的名字叫陈你好,今年17岁,学生2的名字叫张三,今年18岁。
类是一种蓝图,对象是依照蓝图生产出的产品。
构造器
类中的构造器也称为构造方法,构造器通常用来创建对象时进行初始化。
在创建一个对象的时候,至少要调用一个构造方法来完成对象的初始化工作。构造方法的名称必须与类同名,一个类可以有多个构造方法。
// 不存在构造器时隐式给出默认构造器
public Student() {
}
public Student(String name) {
}
需要注意的是:
在我们没有显式的定义一个构造器时,Java会隐式的给出一个无参数的隐式构造器。
this
this关键字用于引用当前对象实例。
引用当前对象的成员变量
当实例变量和方法参数同名时,使用"this"关键字可以明确指示要引用的是当前对象的实例变量而不是方法参数。
public void setName(String name) {
this.name = name;
}
在构造器中调用其他构造器
如果一个类有多个构造方法,可以使用"this"关键字调用其他构造方法。这种方式通常用于避免代码重复。
public Student() {
this("陈你好", 17);
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
返回当前对象的引用
返回当前对象的引用,每次调用后返回该对象,达到链式调用的效果。
public class Student {
private String name;
private int age;
public Student setName(String name) {
this.name = name;
return this;
}
public Student setAge(int age) {
this.age = age;
return this;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
class Main {
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.setName("陈你好").setAge(17).toString());
}
}
在内部类中使用外部类的实例
当在内部类中需要访问外部类的实例时,可以使用"this"关键字引用外部类的实例。
public class OuterClass {
private int x;
public void setX(int x) {
this.x = x;
}
public class InnerClass {
private int x;
public void printOuterX() {
System.out.println("Outer x: " + OuterClass.this.x);
}
}
}
class Main {
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.setX(10);
OuterClass.InnerClass inner = outer.new InnerClass();
inner.printOuterX();
}
}
面向对象的特性
封装
封装是面向对象编程中的一个重要概念,它指的是将数据(字段)和操作数据的方法(方法)打包在一起,并通过访问控制来限制对数据的访问。
封装的主要目的是隐藏类的内部实现细节,使得类的使用者无需了解类的内部实现,只需要通过公共方法与类进行交互。
封装的实现
- 将类的字段声明为私有(private),只能在类的内部访问,外部类无法直接访问这些字段。
- 提供公共(public)方法来访问和修改字段的值,这些方法通常被称为 getter 和 setter 方法。通过这些方法,可以控制对字段的访问,并在需要时执行额外的逻辑(如数据验证、计算等)。
- 使用访问修饰符来限制方法的访问级别,以确保只有需要的方法对外部可见,而内部实现细节被隐藏起来。
public class Student {
// 使用private关键字将属性私有
private String name;
private int age;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
/*
好像提供get/set方法多余了,本质都是给name赋值
思考一个问题,如果不将属性私有,假设外部可以直接操作内部的属性
将age调整为1000岁,name的长度调整为10000,这应该不是我们希望的结果
*/
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = legalAge(age);
}
private int legalAge(int age) {
return age < 0 || age > 200 ? 18 : age;
}
}
组合
面向对象编程中的组合(Composition)是一种重要的对象关系模型,用于描述一个类的实例包含另一个类的实例作为其一部分的情况。
public class Class1 {
private Class2 className;
}
class Class2 {
}
继承
继承是类与类之间的关系,它允许子类继承父类的属性和方法。子类可以重用父类的代码,并且可以在不修改父类代码的情况下添加新的功能或修改已有的行为。
public class Person {
// 字段
private String name;
private int age;
// 构造器
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// getter/setter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void method() {
}
}
子类Student继承父类Person
// 使用extends关键字,让Student类继承Person类
public class Student extends Person {
// 字段
private String clazz;
// 构造器
public Student() {
}
public Student(String name, int age, String clazz) {
super(name, age);
this.clazz = clazz;
}
// getter/setter方法
public String getClazz() {
return clazz;
}
public void setClazz(String clazz) {
this.clazz = clazz;
}
@Override
public void method() {
super.method();
System.out.println("I am student");
}
}
需要注意的是:
- 一个子类只能继承一个父类,一个父类可以被多个子类继承。Java中所有类都继承Object类。
- 子类可以继承父类的公共和受保护成员,但不能继承父类的私有成员。私有成员只能在父类内部访问。
- Java支持多层继承,B类继承A类,C类继承B类…子子孙孙无穷匮也。
super
调用父类的构造器
在子类的构造方法中使用super()
来调用父类的构造方法,以初始化父类的成员变量或执行父类的特定逻辑。
public Student(String name, int age, String clazz) {
super(name, age);
this.clazz = clazz;
}
访问父类的成员变量
如果子类和父类中有同名的成员变量,可以使用super
关键字来访问父类中的成员变量。
public class ParentClass {
String name = "Parent";
}
public class ChildClass extends ParentClass {
String name = "Child";
public void displayName() {
System.out.println("Child class: " + name);
System.out.println("Parent class: " + super.name);
}
}
访问父类的方法
在子类中可以使用super
关键字来调用父类中的方法。
@Override
public void method() {
super.method();
System.out.println("I am student");
}
关于构造器
调用子类构造器默认会隐式调用父类的默认构造器,所以需要确保父类的默认构造器存在。
当父类没有默认构造器,就需要在子类构造器中显式调用父类的有参构造器。
public Student(String name, int age, String clazz) {
super(name, age);
this.clazz = clazz;
}
方法的重写
方法的重写(Override)是面向对象编程中的一个重要概念,它允许子类重新定义父类中已经定义的方法。
子类继承父类,当子类的方法名称、参数列表、返回类型与父类一致时,则会出现方法重写的行为。
@Override
public void method() {
super.method();
System.out.println("I am student");
}
需要注意的是:
- 子类重写的方法的访问修饰符不能比父类的更严格。例如,如果父类方法是
protected
,则子类方法可以是protected
或public
,但不能是private
或[默认不写]
访问级别。 - 子类重写的方法不能抛出比父类方法更广泛的受检查异常。如果父类方法抛出了异常,子类方法可以不抛出异常,或者抛出相同类型的异常或其子类异常。
- 静态方法不能被重写。Java 中重写方法是基于运行时对象的类型来确定调用的方法,而静态方法是与类直接关联的,在编译时就已经确定了调用的具体实现。
- 构造方法不能被重写,但可以被子类的构造方法调用。
多态
多态性是面向对象编程中的一个重要概念,指的是相同的方法可以在不同的对象上产生不同的行为。
当一个对象调用某个方法时,编译器会根据引用变量的类型来确定要调用的方法。但在运行时,调用的是对象的方法,而不是引用变量的方法。
多态的实现
Java的多态性允许一个引用变量来引用不同类型的对象,从而在运行时表现出不同的行为。这通常是通过父类引用子类对象或接口引用实现该接口的类的对象来实现的。
**继承实现多态:**通过子类继承父类并重写父类方法,通过父类引用调用不同类的相同方法,实现多态。
// 父类:Animal
public class Animal {
public void eat() {
System.out.println("eat");
}
public void sleep() {
System.out.println("sleep");
}
}
// 子类:Dog
class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog eat");
}
@Override
public void sleep() {
System.out.println("Dog sleep");
}
}
// 子类:Cat
class Cat extends Animal {
@Override
public void eat() {
System.out.println("cat eat");
}
@Override
public void sleep() {
System.out.println("cat sleep");
}
}
class Main {
// 父类引用子类对象,传入Dog对象,调用Dog类中的eat方法;传入Cat对象,调用Cat类中的eat方法
public static void eat(Animal animal) {
animal.eat();
}
public static void sleep(Animal animal) {
animal.sleep();
}
public static void main(String[] args) {
eat(new Dog());
eat(new Cat());
sleep(new Dog());
sleep(new Cat());
}
}
**接口实现多态:**不同的类实现同一个接口并提供自己的方法实现,通过接口引用调用不同类的相同方法,实现多态。
public interface AnimalBehavior {
void eat();
void sleep();
}
// Dog类实现接口AnimalBehavior
class Dog implements AnimalBehavior {
@Override
public void eat() {
System.out.println("Dog eat");
}
@Override
public void sleep() {
System.out.println("Dog sleep");
}
}
// Cat类实现接口AnimalBehavior
class Cat implements AnimalBehavior{
@Override
public void eat() {
System.out.println("cat eat");
}
@Override
public void sleep() {
System.out.println("cat sleep");
}
}
class Main {
// 接口引用实现该接口的类的对象,传入Dog对象,调用Dog类中的eat方法;传入Cat对象,调用Cat类中的eat方法
public static void eat(AnimalBehavior animal) {
animal.eat();
}
public static void sleep(AnimalBehavior animal) {
animal.sleep();
}
public static void main(String[] args) {
eat(new Dog());
eat(new Cat());
sleep(new Dog());
sleep(new Cat());
}
}
instanceof
instanceof
是 Java 中的一个关键字,用于测试一个对象是否是某个类的实例,或者是否实现了某个接口。
// employee 是要测试的对象,Object 是要检查的类或接口。
if (employee instanceof Object) {
}
需要注意的是:
-
instanceof
检查的是对象的实际类型,而不是引用变量的类型。这意味着即使一个对象是通过父类引用指向的,如果它的实际类型是子类,那么instanceof
也会返回true
。 -
当使用
instanceof
运算符时,以下情况将返回true
:-
对象是指定类的实例。
-
对象是指定接口的实现类的实例。
-
对象是指定类的子类的实例。
-
对象是指定接口的子接口的实现类的实例。
-
Java 16 引入了新的 instanceof 模式匹配语法,允许在一条语句中进行类型检查和强制类型转换。通过在 instanceof 关键字后添加一个变量名,直接在条件块中使用这个变量,避免了手动类型转换,提高了代码的可读性和易用性。
if (employee instanceof Object e) {
}
final
常量
由final关键字修饰的变量被称为常量。首次赋值后将无法再改变其值。
final String name = "陈你好";
需要注意的是:
使用final
修饰一个引用类型的变量时,意味着这个变量不能再指向其他对象,但是并不意味着对象本身的内容不能被修改。
public class Student {
private String name;
public void setName(String name) {
this.name = name;
}
}
class Main {
final Student student = new Student();
student.setName("陈你好");
}
不可覆盖的方法
一个方法被声明为final,则子类不能重写该方法。
public final void method() {
}
不可继承的类
一个类被声明为final,则该类不能被继承。
public final class Math {
}
abstract
抽象方法
在设计一个类的时候,有时候可能想要声明一个方法,但又不希望给出具体的实现,而是留待具体的子类来实现时,就可以将其定义为抽象方法。由abstract关键字修饰,且只有方法声明,没有方法体的方法被称为抽象方法。
public abstract void method();
抽象类
抽象类是一种在面向对象编程中使用的概念,是不能被实例化的类,只能被用作其他类的父类,即被继承。抽象类通常用于定义一些通用的行为和属性,供其子类去实现和扩展。由abstract关键字修饰的类被称为抽象类。
public abstract class Employee {
}
需要注意的是:
- 抽象类需要被继承,抽象方法需要被实现,因此abstract关键字无法于final、private、static关键字一起使用
- 抽象类中可能含有抽象方法,因此无法使用new关键字实例化
- 继承抽象类的子类必须实现抽象类中的所有抽象方法,除非子类也是抽象类
抽象类与类的区别
-
抽象类不能实例化;普通类可以。
-
抽象类中可以包含抽象方法(没有具体实现的方法);普通类中的方法都必须有具体实现
-
子类继承抽象类时,需要实现抽象类中的所有抽象方法,除非子类也是抽象类;普通类的子类可以选择性的覆盖父类的方法
-
抽象类通常用于定义一些通用的行为和属性,供子类去实现和扩展;普通类用于实例化对象,提供具体的功能和特性
接口
接口是一种定义了一系列抽象方法的集合。不同的类可以实现相同的接口,并为每个抽象方法提供具体的实现,从而在不同的类中产生不同的行为。
接口的声明
接口中只存在公共(public)的抽象方法和公共(public)的静态常量字段
// 使用interface关键字声明接口
public interface 接口名 {
// 静态常量字段。在接口中,字段隐式的由public final static修饰,无需显示的定义
public final static String name = "";
// 抽象方法。在接口中,抽象方法隐式的由public abstract修饰,无需显示的定义
public abstract void method();
}
接口中的静态方法
在Java8中,引入了在接口中定义静态方法这一特性。理论上讲,没有任何理由认为这是不合法的。只是这似乎有违于将接口作为抽象规范的初衷。通常的做法是将静态方法放在伴随类中。例如在标准库中,我们会看到成对出现的接口和工具类,Collection/Collections或Path/Paths
public interface 接口名 {
// 静态方法。在接口中,静态方法隐式的由public修饰,无需显示的定义
public static void method1() {
}
// 接口中,静态方法可被public或private两种访问修饰符修饰
private static void method2() {
}
}
接口中的默认方法
在Java8中,引入了在接口中定义方法这一特性。使用default关键字标记默认方法。默认方法只能由public关键字修饰。
有时,并不是所有的方法都需要实现类进行重写实现,就可以定义默认方法让实现类直接"继承"使用即可。
public interface 接口名 {
// 默认方法隐式的由public修饰,无需显示的定义
public default void method() {
}
}
接口的实现
类通过implements关键字实现接口,一个类可以实现多个接口,实现接口的类必须重写所实现的接口中的所有抽象方法。
public class 类名 implements 接口1, 接口2, ...{
}
接口中的继承
接口之间可以通过extends关键字进行继承,子接口继承父接口的所有方法。接口可以多继承,即一个接口可以有多个父接口。
public interface 子接口 extends 父接口1, 父接口2, ...{
}
接口与抽象类
抽象类与接口相比存在一个严重问题,每个类只能扩展一个类。假设Employee类已经扩展了一个类,就不能再扩展另一个类了,即一个子类不能有多个父类
集合
List
ArrayList
ArrayList
是基于动态数组(也叫做可变数组)实现的。其底层实现可以总结如下:
- 数组存储:
ArrayList
底层使用一个数组来存储元素。初始时,数组有一个默认容量,通常是10。 - 动态扩容:当添加新元素导致数组容量不足时,
ArrayList
会自动扩容。扩容的过程是创建一个新的数组,其容量通常是旧数组容量的1.5倍或2倍,然后将旧数组的元素复制到新数组中。 - 快速访问:由于底层是数组,
ArrayList
支持通过索引快速访问元素,时间复杂度为O(1)。 - 插入和删除:在中间插入或删除元素需要移动后续元素,时间复杂度为O(n),其中n是数组的大小。
LinkedList
LinkedList
是基于双向链表实现的。其底层实现可以总结如下:
- 双向链表结构:
LinkedList
使用节点(Node)来存储元素,每个节点包含一个数据域(data)和两个指针(前驱指针prev和后继指针next),分别指向前一个节点和后一个节点。 - 动态存储:与数组不同,链表不需要预先分配固定大小的内存。每添加一个元素,就创建一个新的节点并链接到链表中。
- 插入和删除:在链表的任意位置插入或删除元素的时间复杂度为O(1),因为只需要调整相邻节点的指针。然而,定位到某个位置的时间复杂度为O(n),因为需要从头开始遍历链表。
- 访问元素:访问链表中的元素通常需要从头或尾开始遍历,时间复杂度为O(n)。
对比
- 访问速度:
ArrayList
由于使用数组存储,可以通过索引快速访问元素,而LinkedList
需要遍历链表,访问速度较慢。 - 插入和删除速度:
LinkedList
在插入和删除操作方面更高效,尤其是在中间位置插入和删除,而ArrayList
则需要移动大量元素。 - 适用场景:
ArrayList
适用于频繁访问元素的场景,而LinkedList
适用于频繁插入和删除元素的场景。
Set
List
接口表示一个有序的集合,允许包含重复的元素。Set
接口表示一个无序不包含重复元素的集合。
HashSet
HashSet
是基于哈希表(HashMap)实现的集合类,它不允许存储重复元素,并且没有特定的顺序。
底层实现
- 哈希表(HashMap):
HashSet
底层使用HashMap
来存储元素。每个元素作为HashMap
的键,HashMap
的值是一个固定的常量对象。 - 哈希函数:
HashSet
使用元素的hashCode()
方法来计算元素的哈希值,并通过哈希值决定元素在表中的位置。 - 处理哈希冲突:
HashMap
采用链地址法(链表或红黑树)来处理哈希冲突。当哈希冲突较少时,冲突的元素存储在链表中;当冲突较多时,链表转换为红黑树以提高查询效率。 - 加载因子和扩容:当哈希表的元素数量超过阈值(容量 * 加载因子)时,哈希表会扩容并重新哈希已有的元素,新的容量通常是旧容量的两倍。
LinkedHashSet
LinkedHashSet
是 HashSet
的子类,除了哈希表,还维护了一个双向链表以保证元素的插入顺序。
底层实现
- 哈希表(HashMap):与
HashSet
类似,LinkedHashSet
也使用HashMap
来存储元素。 - 双向链表:
LinkedHashSet
通过在HashMap
的基础上,维护一个双向链表来记录元素的插入顺序。每个节点不仅包含哈希表中的键值对,还包含指向前一个和后一个节点的指针。 - 保持插入顺序:通过双向链表,
LinkedHashSet
可以保证遍历时按照元素的插入顺序进行。
HashSet和LinkedHashSet的工作原理
使用HashSet、LinkedHashSet存储自定义类的实例时,自定义类需要重写equals()和hashCode()方法。重写equals()确保Set集合的无序性,重写hashCode()用于生成对象的哈希码,哈希码确定对象在哈希表中的位置。如果两个对象通过equals()比较相等,则它们的hashCode也必须相等。
- 哈希表:
HashSet
和LinkedHashSet
使用哈希表来存储元素。每个元素通过hashCode
方法生成一个哈希码,根据哈希码决定元素的存储位置。 - 哈希冲突:如果两个对象的哈希码相同,则会发生哈希冲突。此时,
HashSet
和LinkedHashSet
会进一步使用equals
方法来比较对象,判断是否真的是相同对象(即使哈希码相同,不一定是相同对象)。 - 唯一性:通过
hashCode
和equals
方法,HashSet
和LinkedHashSet
能够保证集合中不包含重复的元素。
TreeSet
TreeSet
是基于红黑树(Red-Black Tree)实现的集合类,元素是有序的,默认按自然顺序排序,也可以使用指定的比较器(Comparator)进行排序。
底层实现
- 红黑树(TreeMap):
TreeSet
底层使用TreeMap
来存储元素。每个元素作为TreeMap
的键,TreeMap
的值是一个固定的常量对象。 - 排序:红黑树是一种自平衡的二叉搜索树,通过在插入和删除时进行旋转和重新着色来保持树的平衡,从而保证了
TreeSet
中元素的有序性。 - 搜索、插入、删除:在红黑树中,这些操作的时间复杂度均为 O(log n)。
注意
使用TreeSet存储自定义类的实例时,需要自定义类需要实现Comparable接口的compareTo方法,或者在定义TreeSet时,给TreeSet构造器传递一个实现了Comparator接口的实例。这样才能确保 TreeSet
能够正确地对元素进行排序。
总结
- HashSet:
- 基于哈希表实现。
- 快速查找、插入和删除(平均时间复杂度 O(1))。
- 元素无序。
- LinkedHashSet:
- 基于哈希表和双向链表实现。
- 保证插入顺序。
- 快速查找、插入和删除(平均时间复杂度 O(1)),但有额外的内存开销。
- TreeSet:
- 基于红黑树实现。
- 元素有序。
- 查找、插入和删除的时间复杂度为 O(log n)。
Map
HashMap
HashMap
是基于哈希表(hash table)实现的,它提供了高效的查找、插入和删除操作。
底层实现
-
哈希表:
HashMap
使用一个数组(桶)来存储键值对,每个桶对应一个链表或红黑树(链地址法处理哈希冲突)。 -
哈希函数:键的
hashCode
方法用于计算哈希值,决定元素在哈希表中的位置。 -
哈希冲突处理
:当两个键的哈希值相同(哈希冲突)时,元素存储在同一个桶的链表或红黑树中。
- 链表:当冲突较少时,使用链表存储冲突的元素。
- 红黑树:当冲突较多时,链表转换为红黑树,以提高查询效率。
特性
- 时间复杂度:查找、插入和删除操作的平均时间复杂度为 O(1)。
- 无序存储:
HashMap
中的元素没有特定的顺序。 - 允许 null:可以存储一个
null
键和多个null
值。 - 非线程安全:多个线程同时访问和修改
HashMap
可能导致数据不一致。
LinkedHashMap
LinkedHashMap
是 HashMap
的子类,具有 HashMap
的所有特性,并且维护了插入顺序或访问顺序。
底层实现
- 哈希表:与
HashMap
相同,使用数组和链表或红黑树处理哈希冲突。 - 双向链表:维护元素的插入顺序或访问顺序。每个元素还包含前后指针,形成一个双向链表。
特性
- 有序存储:按照插入顺序或访问顺序存储元素。
- 时间复杂度:与
HashMap
相同,查找、插入和删除操作的平均时间复杂度为 O(1)。 - 允许 null:可以存储一个
null
键和多个null
值。 - 非线程安全:多个线程同时访问和修改
LinkedHashMap
可能导致数据不一致。
TreeMap
TreeMap
是基于红黑树(Red-Black Tree)实现的,键值对按照键的自然顺序或指定的比较器顺序进行排序。
底层实现
- 红黑树:一种自平衡二叉搜索树,通过节点的旋转和重新着色来保持平衡。
- 排序:默认情况下,键按其自然顺序排序。如果提供了
Comparator
,则按Comparator
的顺序排序。
特性
- 有序存储:键值对按照键的顺序存储和迭代。
- 时间复杂度:查找、插入和删除操作的时间复杂度为 O(log n)。
- 不允许 null:
TreeMap
不允许null
键,但允许null
值。 - 非线程安全:多个线程同时访问和修改
TreeMap
可能导致数据不一致。
Hashtable
Hashtable
是基于哈希表实现的,与 HashMap
类似,但它是线程安全的,适用于多线程环境。
底层实现
- 哈希表:使用数组和链表处理哈希冲突。
- 同步:所有方法都使用
synchronized
关键字进行同步,确保线程安全。
特性
- 线程安全:所有方法都是同步的,适用于多线程环境。
- 不允许 null:不允许
null
键和null
值。 - 时间复杂度:查找、插入和删除操作的平均时间复杂度为 O(1)。
- 无序存储:元素没有特定的顺序。
Properties
Properties
是 Hashtable
的子类,专门用于处理属性文件(键和值都是字符串)。
底层实现
- 哈希表:与
Hashtable
相同,使用数组和链表处理哈希冲突。 - 字符串键值对:键和值都必须是字符串(
String
)。
特性
- 专用于属性文件:键和值都必须是字符串,常用于读取和保存配置文件。
- 线程安全:继承了
Hashtable
的同步特性,适用于多线程环境。 - 不允许 null:不允许
null
键和null
值。 - 无序存储:元素没有特定的顺序。
- 额外功能:提供
load
和store
方法,用于从输入流读取属性文件或将属性保存到输出流。
比较与选择
- HashMap:适用于对顺序不敏感且需要高效查找和更新操作的场景。
- LinkedHashMap:适用于需要维护插入顺序或访问顺序的场景。
- TreeMap:适用于需要有序存储和快速范围查询的场景。
- Hashtable:适用于需要线程安全的多线程环境,但不允许
null
键和值。 - Properties:适用于读取和保存配置文件,键和值都必须是字符串。
泛型
泛型也称参数类型,起到标识和约束作用。通过泛型处理不同类型对象的类、接口和方法时,无需显式地进行类型转换。
泛型类
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
使用泛型类
Box<String> stringBox = new Box<>();
stringBox.setValue("Hello");
String value = stringBox.getValue();
泛型方法
public <E> E method(E e) {
return e;
}
public <E> void voidMethod(E e) {
}
需要注意的是:
创建自定义泛型类的对象,没有指明泛型参数类型时,按照Object处理,但不等价于Object
泛型不能是基本类型,只能是引用类型
泛型不能用于异常
泛型可以有多个,逗号隔开。如Map<K, V>
泛型通配符
<?>
被称为无界通配符,表示任意类型。它常用于方法参数中,表示该方法可以接受任何类型的参数,而无需关心具体的类型是什么。
当你只需要读取数据,而不需要修改数据时,可以使用无界通配符。例如,打印集合中的所有元素。
通配符 | 描述 |
---|---|
? | 表示任何类型 |
? extends T | 表示类型是T或T的子类型 |
? super T | 表示类型是T或T的超类型 |
内部类
内部类就是在一个类的内部再定义一个类,例如,A类中定义一个B类,那么B类相对于A类来说就被称为内部类,而A类相对B类就被成为外部类。使用内部类主要有两个原因:
- 将内部类声明为私有或默认访问级别,内部类就只能被外部类或同一个包中的其他类访问。
- 在内部类的方法中,可以直接访问外部类方法中声明的私有变量或私有方法。
在Java中,内部类分为四种类型:成员内部类、静态内部类、局部内部类和匿名内部类。
**关于访问修饰符:**与方法一样,我们也可以使用访问修饰符控制内部类的可见性
成员内部类
定义在类内部且不使用 static 修饰的内部类。
public class OuterClass {
class InnerClass {
}
}
创建内部类实例
// 创建一个外部类实例
OuterClass outerClass = new OuterClass();
// 创建一个外部类中的内部类实例
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
外部类与内部类之间的访问
使用内部类类名访问内部类中的静态成员。
使用外部类类名访问外部类中的静态成员。
在非静态方法中,外部类可以通过实例化内部类,使用内部类对象访问内部类中的非静态成员;内部类可以直接访问外部类中的非静态成员。
在静态方法中,外部类可以通过外部类对象实例化内部类,使用内部类对象访问内部类中的非静态成员;内部类可以通过实例化外部类,使用外部类对象访问外部类非静态成员。
public class OuterClass {
private void method() {
// 使用内部类对象访问内部类非静态成员(完整形式)
// OuterClass outer = this;
// InnerClass inner = outer.new InnerClass();
// 使用内部类对象访问内部类非静态成员
InnerClass inner = new InnerClass();
inner.method();
// 使用内部类类名访问内部类静态成员
InnerClass.staticMethod();
}
public static void staticMethod() {
// 使用内部类对象访问内部类非静态成员
OuterClass outer = new OuterClass();
InnerClass inner = outer.new InnerClass();
inner.method();
// 使用内部类类名访问内部类静态成员
InnerClass.staticMethod();
}
private class InnerClass {
public void method() {
// 直接访问外部类非静态成员
OuterClass.this.method();
// 使用外部类类名访问外部类静态成员
OuterClass.staticMethod();
}
public static void staticMethod() {
// 使用外部类对象访问外部类非静态成员
OuterClass outer = new OuterClass();
outer.method();
// 使用外部类类名访问外部类静态成员
OuterClass.staticMethod();
}
}
}
静态内部类
如果内部类不需要访问外部类非静态成员,那么可以使用static关键字修饰,将其定义为静态内部类,有助于降低耦合性(耦合性是软件工程中的一个概念,指的是模块或组件之间的相互依赖程度)。
public class OuterClass {
static class InnerClass {
}
}
创建内部类实例
// 创建一个外部类中的内部类实例
OuterClass.InnerClass inner = new OuterClass.InnerClass();
外部类与内部类之间的访问
public class OuterClass {
public void method() {
// 使用内部类对象访问内部类非静态成员
InnerClass innerClass = new InnerClass();
innerClass.method();
// 使用内部类类名访问内部类静态成员
InnerClass.staticMethod();
}
public static void staticMethod() {
// 使用内部类对象访问内部类非静态成员
InnerClass innerClass = new InnerClass();
innerClass.method();
// 使用内部类类名访问内部类静态成员
InnerClass.staticMethod();
}
static class InnerClass {
public void method() {
// 使用外部类对象访问外部类非静态成员
OuterClass outerClass = new OuterClass();
outerClass.method();
// 使用外部类类名访问外部类静态成员
OuterClass.staticMethod();
}
public static void staticMethod() {
// 使用外部类对象访问外部类非静态成员
OuterClass outerClass = new OuterClass();
outerClass.method();
// 使用外部类类名访问外部类静态成员
OuterClass.staticMethod();
}
}
}
成员内部类与静态内部类的区别
访问外部类成员
- 成员内部类可以直接访问外部类的成员变量和方法,因为它隐式地持有一个对外部类对象的引用。
- 静态内部类不能直接访问外部类的非静态成员,它只能访问外部类的静态成员。这是因为静态内部类没有对外部类对象的引用,它与外部类的实例无关。
对于外部类实例的依赖
- 成员内部类依赖于创建它的外部类实例,因此不能在没有外部类实例的情况下被创建。
- 静态内部类与外部类实例无关,可以独立存在。它可以像普通类一样被实例化,并且不需要依赖外部类实例。
关于内存
- 每个成员内部类都会隐式保存一个对外部类对象的引用,因此会占用额外的内存。
- 静态内部类不会隐式保存外部类对象的引用,因此在内存上占用更少的空间。
局部内部类
当一个类仅在一个方法中使用,且仅需要一次性的实例时,局部内部类提供了一种简洁的实现方式。
局部内部类是定义在方法体或作用域内部的类,局部内部类不能使用访问修饰符修饰,只能在定义它的方法中或作用域内使用。
public class OuterClass {
public void outerMethod() {
class InnerClass {
}
}
}
访问当前方法中的局部变量
与其他内部类相比,局部内部类不仅能够访问外部类的成员,还可以访问该作用域中的局部变量。不过,这些局部变量必须是final 或 effectively final(一个术语,用于表述在实际情况中表现的像是final修饰的变量。一个局部变量在初始化之后,没有被重新赋值,即使它没有被声明为final,也可以视为effectively final。)
匿名内部类
在使用局部内部类时,假设只想创建该类的一个对象,可以进一步简化,省略该类的名字。这样的类被称为匿名内部类。
匿名内部类大致分为两种形式:
作为接口的实现类
InterfaceName obj = new InterfaceName() {
// 实现接口方法
};
作为抽象类的子类
AbstractClassName obj = new AbstractClassName() {
// 实现抽象方法
};
需要注意的是:
匿名内部类没有构造器,因为它没有类名。它的定义和创建通常放在一起,所以也只能创建一个实例。
尽管匿名类没有构造器,但我们可以提供一个对象初始化块,用作构造器的替代。
异常
因为各种各样的原因,程序运行过程中可能会出现不期而至的各种状况(错误),如文件找不到、网络连接失败、非法参数等。这些错误被称为异常。
异常的分类
Java将异常当作对象来处理,并定义了一个基类java.lang.Throwable。Throwable有两个子类(Error、Exception)。
Java语言将派生于Error类或RuntimeException类的所有异常称为非检查型异常,将其他异常称为检查型异常。
Error
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽问题。出现Error类型的对象时,我们除了通知用户,并尽力妥善地终止程序之外,几乎无能为力。这些错误出现在应用程序的控制和处理范围之外,我们要避免出现Error。
Exception
Exception类层次结构描述了程序中可能遇到的各种异常情况。Exception层次结构分为两个分支:RuntimeException异常和其他异常。Exception层次结构中的异常一般是由程序的逻辑错误引起的,我们应该从逻辑角度尽可能避免这类异常的发生。Exception通常情况是可以被程序处理的。
声明异常
使用throws关键字声明异常(声明的异常可以有多个),告诉调用者该方法可能会出现哪些异常。
public static void division(int a, int b) throws ArithmeticException, ... {
System.out.println(a / b);
}
需要注意的是:
不需要声明非检查型异常,一方面,从Error继承的异常我们处理不了;另一方面,从RuntimeException继承的异常我们完全可以通过优化程序逻辑来避免。
抛出异常
在 Java 中,可以使用throw关键字来手动抛出异常。
throw 异常对象;
自定义异常
- 定义一个类并继承Exception或Exception的子类
- 定义两个构造器,一个默认构造器和一个包含详细描述信息的构造器
public class MyException extends Exception {
public MyException() {
}
public MyException(String message) {
super(message);
}
}
捕获异常
try-catch
如果try语句块中的代码抛出了catch子句中指定的一个异常类,程序将跳过try语句块中的其余代码,执行catch子句中的处理器代码。catch子句可以有多个。
try {
} catch (FileNotFoundException e) {
} catch (IOException e) {
}
finally子句
无论是否捕获到异常,finally子句都会执行。常用来关闭资源(这里的资源是指文件等等,拿BufferedReader举例,调用该类对象的close()方法时,关闭的是该对象的源,而不是该对象;使用IO流时,每个资源只能访问一次)。
try {
} catch (FileNotFoundException e) {
} catch (IOException e) {
} finally {
}
try-with-Resources
在try()中定义的资源(这些资源必须实现AutoCloseable或Closeable接口),在try块退出时,会自动调用close()方法关闭资源,简化finall代码块
try (Scanner scanner = new Scanner(System.in)) {
System.out.println(scanner.nextInt());
} catch (Exception e) {
throw new RuntimeException(e);
}
在JDK9中,可以在try之上定义流对象,在try()中引用流对象的名称,在try执行完毕后,流对象也可以释放掉。
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
try(br;bw) {
String s = br.readLine();
System.out.println(s);
bw.write(s, 0, s.length());
} catch (IOException e) {
throw new RuntimeException(e);
}
lambda
对代码进行简化,只关注结果,不关注过程。
函数式接口
接口中只有一个抽象方法,该接口被称为函数式接口
lambda对函数式接口的简化
import org.junit.jupiter.api.Test;
public class DemoTest {
/**
* 函数式接口:无参、无返回值
*/
@Test
public void test1() {
@FunctionalInterface
interface FunctionInterface {
void method();
}
// 匿名内部类实现
FunctionInterface f1 = new FunctionInterface() {
@Override
public void method() {
System.out.println("Hello World");
}
};
// lambda简化
FunctionInterface f2 = () -> {
System.out.println("Hello World");
};
// 当匿名内部类的实现只有一句代码时,lambda可以进一步简化
FunctionInterface f3 = () -> System.out.println("Hello World");
}
/**
* 函数式接口:有参、无返回值
*/
@Test
public void test2() {
@FunctionalInterface
interface FunctionInterface<T> {
void method(T t);
}
// 匿名内部类实现
FunctionInterface<String> f1 = new FunctionInterface<String>() {
@Override
public void method(String s) {
System.out.println("Hello " + s);
}
};
// lambda简化
FunctionInterface<String> f2 = (String s) -> {
System.out.println("Hello " + s);
};
// 当匿名内部类的实现只有一句代码时,lambda可以进一步简化
FunctionInterface<String> f3 = (String s) -> System.out.println("Hello " + s);
// 当匿名内部类的方法参数类型与接口泛型一致时,lambda可以进一步简化
FunctionInterface<String> f4 = (s) -> System.out.println("Hello " + s);
// 当匿名内部类的方法参数与接口泛型一致,且只有一个参数,lambda可以进一步简化
FunctionInterface<String> f5 = s -> System.out.println("Hello " + s);
}
/**
* 函数式接口:无参、有返回值
*/
@Test
public void test3() {
@FunctionalInterface
interface FunctionInterface<T> {
T method();
}
// 匿名内部类实现
FunctionInterface<String> f1 = new FunctionInterface<>() {
@Override
public String method() {
return "Hello World";
}
};
// lambda简化
FunctionInterface<String> f2 = () -> {
return "Hello World";
};
// 当匿名内部类的实现只有一句return语句时,lambda可以进一步简化
FunctionInterface<String> f3 = () -> "Hello World";
}
/**
* 函数式接口:有参、有返回值
*/
@Test
public void test4() {
@FunctionalInterface
interface FunctionInterface<T> {
T method(T t);
}
// 匿名内部类实现
FunctionInterface<String> f1 = new FunctionInterface<>() {
@Override
public String method(String s) {
return "Hello " + s;
}
};
// lambda简化
FunctionInterface<String> f2 = (String s) -> {
return "Hello " + s;
};
// 当匿名内部类的实现只有一句return语句时,lambda可以进一步简化
FunctionInterface<String> f3 = (String s) -> "Hello " + s;
// 当匿名内部类的方法参数类型与接口泛型一致时,lambda可以进一步简化
FunctionInterface<String> f4 = (s) -> "Hello " + s;
// 当匿名内部类的方法参数与接口泛型一致,且只有一个参数,lambda可以进一步简化
FunctionInterface<String> f5 = s -> "Hello " + s;
}
}
4个基本函数式接口
接口 | 抽象方法 |
---|---|
Consumer<T> | void accept(T t) |
Supplier<T> | T get() |
Function<T, R> | R apply(T t) |
Predicate<T> | boolean test(T t) |
方法引用
lambda简化的语句满足一定条件,进一步简化
对象::实例方法
import org.junit.jupiter.api.Test;
public class DemoTest {
/**
* 方法引用:有参、无返回值
*/
@Test
public void test1() {
@FunctionalInterface
interface FunctionInterface<T> {
void method(T t);
}
// 匿名内部类实现
FunctionInterface<String> f1 = new FunctionInterface<>() {
@Override
public void method(String s) {
System.out.println(s);
}
};
// lambda简化
FunctionInterface<String> f2 = s -> System.out.println(s);
/*
内部实现调用的方法的参数与函数式接口的抽象方法方法参数一致、返回类型也一致时,可进一步简化
println()的参数与method()一致,返回类型都是void
*/
FunctionInterface<String> f3 = System.out::println;
}
/**
* 方法引用:有参、有返回值
*/
@Test
public void test2() {
@FunctionalInterface
interface FunctionInterface<T> {
T method(T t1, T t2);
}
// 内部类提供方法
class GetFirst {
public String getFirst(String s1, String s2) {
return s1;
}
}
// 内部类实现
FunctionInterface<String> f1 = new FunctionInterface<>() {
@Override
public String method(String s1, String s2) {
return new GetFirst().getFirst(s1, s2);
}
};
// lambda简化
FunctionInterface<String> f2 = (s1, s2) -> new GetFirst().getFirst(s1, s2);
/*
内部实现调用的方法的参数与函数式接口的抽象方法方法参数一致、返回类型也一致时,可进一步简化
getFirst()的参数与method()一致,返回类型都是String
*/
FunctionInterface<String> f3 = new GetFirst()::getFirst;
}
}
类::静态方法
import org.junit.jupiter.api.Test;
public class DemoTest {
/**
* 方法引用:无参、无返回值
*/
@Test
public void test1() {
@FunctionalInterface
interface FunctionInterface {
void method();
}
// 内部类提供方法
class HelloWorld {
public static void print() {
System.out.print("Hello World");
}
}
// 内部类实现
FunctionInterface f1 = new FunctionInterface() {
@Override
public void method() {
HelloWorld.print();
}
};
// lambda简化
FunctionInterface f2 = () -> HelloWorld.print();
/*
内部实现调用的方法的参数与函数式接口的抽象方法方法参数一致、返回类型也一致时,可进一步简化
print()的参数与method()一致,返回类型都是void
*/
FunctionInterface f3 = HelloWorld::print;
}
/**
* 方法引用:有参、有返回值
*/
@Test
public void test2() {
@FunctionalInterface
interface FunctionInterface<T> {
T method(T t1, T t2);
}
// 内部类实现
FunctionInterface<Integer> f1 = new FunctionInterface<>() {
@Override
public Integer method(Integer o1, Integer o2) {
return Integer.max(o1, o2);
}
};
// lambda简化
FunctionInterface<Integer> f2 = (o1, o2) -> Integer.max(o1, o2);
/*
内部实现调用的方法的参数与函数式接口的抽象方法方法参数一致、返回类型也一致时,可进一步简化
max()的参数与method()一致,返回类型都是Integer,Integer与int不影响,自动装箱了
*/
FunctionInterface<Integer> f3 = Integer::max;
}
}
类::实例方法
import org.junit.jupiter.api.Test;
public class DemoTest {
/**
* 方法引用:有参、无返回值
*/
@Test
public void test1() {
@FunctionalInterface
interface FunctionInterface<T> {
void method(T t);
}
// 内部类提供方法
class HelloWorld {
public void print() {
System.out.println("Hello World");
}
}
// 匿名内部类实现
FunctionInterface<HelloWorld> f1 = new FunctionInterface<>() {
@Override
public void method(HelloWorld h) {
h.print();
}
};
// lambda简化
FunctionInterface<HelloWorld> f2 = h -> h.print();
/*
函数式接口中的抽象方法与其内部实现时调用的方法返回类型相同,其中抽象方法中的n个参数,
其中第一个参数作为方法调用者,其余的n-1个参数与内部实现时调用的方法的参数一致
*/
FunctionInterface<HelloWorld> f3 = HelloWorld::print;
}
/**
* 方法引用:有参、有返回值
*/
@Test
public void test2() {
@FunctionalInterface
interface FunctionInterface<T> {
boolean method(T t1, T t2);
}
// 内部类实现
FunctionInterface<String> f1 = new FunctionInterface<>() {
@Override
public boolean method(String s1, String s2) {
return s1.equals(s2);
}
};
// lambda简化
FunctionInterface<String> f2 = (s1, s2) -> s1.equals(s2);
/*
函数式接口中的抽象方法与其内部实现时调用的方法返回类型相同,其中抽象方法中的n个参数,
其中第一个参数作为方法调用者,其余的n-1个参数与内部实现时调用的方法的参数一致
*/
FunctionInterface<String> f3 = String::equals;
}
}
构造器引用
import org.junit.jupiter.api.Test;
public class DemoTest {
/**
* 构造器引用:无参构造器
*/
@Test
public void test1() {
@FunctionalInterface
interface FunctionInterface<T> {
T get();
}
// 内部类提供无参构造器
class HelloWorld {
}
// 内部类实现
FunctionInterface<HelloWorld> f1 = new FunctionInterface<>() {
@Override
public HelloWorld get() {
return new HelloWorld();
}
};
// lambda简化
FunctionInterface<HelloWorld> f2 = () -> new HelloWorld();
// 内部实现只有一句return时,可以进一步简化
FunctionInterface<HelloWorld> f3 = HelloWorld::new;
}
/**
* 构造器引用:有参构造器
*/
@Test
public void test2() {
@FunctionalInterface
interface FunctionInterface<T, R> {
R get(T t);
}
// 内部类提供一个有参构造器
class HelloWorld {
public HelloWorld(int id) {
}
}
// 匿名内部类实现
FunctionInterface<Integer, HelloWorld> f1 = new FunctionInterface<Integer, HelloWorld>() {
@Override
public HelloWorld get(Integer id) {
return new HelloWorld(id);
}
};
// lambda简化
FunctionInterface<Integer, HelloWorld> f2 = id -> new HelloWorld(id);
// 内部实现只有一句return时,可以进一步简化
FunctionInterface<Integer, HelloWorld> f3 = HelloWorld::new;
}
}
枚举类
枚举类(Enum Class)是 Java 中的一种特殊类,用于定义一组常量。它是一种自定义的类型,可以用于表示一组固定的、预定义的值。
1. 基本概念
- 枚举类:一个枚举类使用
enum
关键字来定义,而不是class
关键字。枚举类的实例是枚举值(常量),每个枚举值都是该枚举类的一个实例。 - 枚举值:枚举类中定义的常量(枚举值)是枚举类的唯一实例,且默认是
public static final
类型的。
2. 枚举类的定义
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}
3. 枚举类的特性
枚举类可以想普通类一样定义方法,重写方法,定义字段。
不同的是,
在普通类中定义的字段是作用于该字段本身的,但在枚举类中定义的字段是作用于枚举类中的所有枚举值。就像每个枚举值都是一个对象,定义的私有字段都是它的属性。
在枚举类中定义的构造函数必须是private关键字修饰的。
public enum Day {
// 作用于枚举值
MONDAY(1, "Monday"),
TUESDAY(2, "Tuesday");
private int dayNumber;
private String dayName;
// 构造函数隐式的使用了private关键字修饰
Day(int dayNumber, String dayName) {
this.dayNumber = dayNumber;
this.dayName = dayName;
}
public int getDayNumber() {
return dayNumber;
}
public String getDayName() {
return dayName;
}
}
4. 枚举类的高级特性
除了像普通类一样实现抽象方法外,枚举类还允许让每个枚举值实现接口的抽象方法,使其为每个枚举值提供不同的行为。
public interface Printable {
void print();
}
public enum Document implements Printable {
PDF {
public void print() {
System.out.println("Printing PDF");
}
},
WORD {
public void print() {
System.out.println("Printing WORD");
}
}
}
每个枚举值(PDF
和 WORD
)都实现了 print()
方法。当你调用 Document.PDF.print()
时,它会打印出 “Printing PDF”。
5. 枚举的局限性
-
枚举类不能继承其他类(因为它隐式继承自
java.lang.Enum
),但可以实现接口。 -
枚举类的常量是静态的、不可变的,因此不适合表示需要动态变化的值。
拓展
jShell
利用jShell,在没有创建类的情况下,在命令行中声明变量、计算表达式、执行语句。命令行中键入jshell
进入 jshell 工具。
jShell常用命令:
/help # 获取有关使用jshell工具的帮助
/help intro # jShell工具的简介
/list # 列出当前session里所有有效的代码片段
/vars # 列出当前session下所有创建过的变量
/methods # 列出当前session下所有创建过的方法
/imports # 列出导入的包
/history # 列出键入的内容的历史记录
/edit # 使用外部代码编辑器来编写 Java 代码
/exit # 退出 jshell 工具