文章目录
- 记JAVA相关资料——基础知识
- 基础常识
- 基础语法
- 基本数据类型
- 引用数据类型
- 字符串(String、Stringbuffer、Stringbuilder)
- 数字(Random、Math、DecimalFormat、BigInteger、BigDecimal)
- 时间(Date、Calendar、String、long、SimpleDateFormat、java.time包)
- 数组(xxx[]、Arrays、比较器)
- 集合(Collection、List、Set、Map、Collections、Stream)
- 文件(File、RandomAccessFile、Paths、Path、Files、URL、URI)
- IO(InputStream、OutputStream、Reader、Writer)
- 线程(Thread、Runable、Callable、ExecutorService、CompletableFuture)
- 锁(synchronized、volatile、CAS、lock、LockSupport、StampedLock)
- 通信(javax.servlet包、javax.websocket包、javax.mail包、java.net包、)
- 常见机制
- JDK8新特性
记JAVA相关资料——基础知识
我电脑win10系统,java版本是8.0的。
基础常识
注释、标识符、关键字、直接量、常量、变量、数据类型、强类型语言
注释相关知识
注释,顾名思义:标注解释。
在java中,注释的作用有三:解释代码、屏蔽代码、生成API文档
在Java中,注释的种类有三:文档注释/***/
、多行注释/**
、单行注释//
使用展示:
/**
* 这是一个文档注释
* 可以跨越多行
* 用于描述类、 接口、方法的作用、功能、参数
* 可以用途生成API文档
*/
/*
* 这是一个多行注释
* 可以跨越多行
* 用于描述代码块作用。
*/
// 这是单行注释,用于描述该行代码用途,
解释代码:
/**
* 测试类
*/
public class MyDemo {
// 声明一个int变量
int x = 10; // 初始化变量x为10
/*
* 声明int变量y
* 给变量赋值20
*/
int y = 20;
/**
* 计算两个整数的和
* @param a 第一个整数
* @param b 第二个整数
* @return 两个整数的和
*/
public int add(int a, int b) {
return a + b;
}
}
屏蔽代码:
/**
* 测试类
*/
public class MyDemo {
public static void main(String[] args) {
// print();
}
/* public static void print() {
System.out.println("打印xxx");
}*/
}
标识符相关知识
标识符,顾名思义:标记识别的符号。简单来讲,就是起名字。
定义:标识符是用来标识变量、方法、类、接口和其他用户自定义元素的名称。
命名规则:标识符必须以字母、下划线、美元符号开头,后面可以跟着字母、数字、下划线和美元符号。
注意事项:标识符区分大小写、不能是Java关键字、无长度限制
命名规范:见名知意(具有描述性)、驼峰命名法(类名、方法名、变量名)、下划线命名法(常量)
使用展示:
/**
* 见名知意:使用英文单词,不随意命名abcd
* 驼峰命名法:单词首字母大写
* 下划线命名法:单词使用下划线拼接
*/
/**
* 常量名:用来表示不可变的值。
* 命名规范:字母全部大写,单词间用下划线隔开。
*/
final int MAX_VALUE = 100;
/**
* 变量名:用来表示存储数据的容器。
* 命名规范:使用首字母小写的驼峰法命名
*/
int age;
String studentName;
/**
* 方法名:用来表示执行特定操作的代码块。
* 命名规范:使用首字母小写的驼峰法命名
*/
public void printMessage() {
System.out.println("Hello, World!");
}
/**
* 类名:用来表示对象的抽象模板。
* 命名规范:使用首字母大写的驼峰法命名
*/
public class MyClass {
// 类的定义
}
/**
* 接口名:用来表示抽象的规范。
* 命名规范:使用首字母大写的驼峰法命名
*/
public interface MyInterface {
// 接口的定义
}
关键字相关知识
定义:java中的关键字是预定义的标识符,一律用小写字母标识,具有特殊的含义和用途。
注意事项:关键字在Java语言中有固定的定义和用法,不能用作变量名、方法名、类名、包名和参数。
用途:在Java中,其用途诸多:用于基本数据类型,用于语句,用于修饰。用于方法、包、类、接口、异常等。
补充:Java中关键字中含有两个保留字,其尚未被使用。
使用展示:
/**
* 用于基本数据类型:
* byte、short、int、long、float、double、char、boolean
*/
/**
* 用于语句:
* if语句: if、else
* swtich语句:switch、case、break、default
* for语句: for、continue
* while语句: do、while、continue
* 异常语句:try、catch、finally
* 结束语句:continue、break、return
*/
/**
* 用于修饰:
* private、public、static、final、
* abstract、protected、synchronized、……
*/
/**
* 用于方法:void、return、throws
* 用于类:class、extends
* 用于接口:interface、implements
* 用于包:package、import、
* 用于异常:throw、throws、
* 用于指代:this、super
* 用于值:true、false、null
* 用于运算:instanceof
* ...
*/
/**
* 保留字:goto、const
*/
直接量相关知识
直接量是指在代码中直接出现的值,不需要经过计算或处理即可使用。在Java中,字面量和常量值都可以被视为直接量
补充:字面量、常量值、默认值
字面量是用来表示固定值的语法元素,代表着一些固定的值,如整数、浮点数、字符、布尔值、字符串、空值。
常量值是指在程序运行过程中不会发生变化的值。可以由字面量表示,也可以由表达式计算得出。常量值又称字面常量。
默认值,当声明一个变量时,如果没有显式地为其赋值,Java会为其赋予默认值。仅限于成员变量。局部变量需要赋值才能使用。
补充:Java还支持使用特殊的前缀或后缀来表示不同的进制或数据类型。
例如,整数字面量可以使用前缀0x表示十六进制,前缀0表示八进制,或者使用后缀L表示long类型。浮点数字面量可以使用后缀f表示float类型。
使用展示:
/**
* 整数字面量:表示整数值的字面量,可以是十进制、八进制或十六进制。默认是int类型
*/
int a = 10; // 十进制整数10
int b = 012; // 八进制整数10
int c = 0xA; // 十六进制整数10
/**
* 浮点数字面量:表示浮点数值的字面量,可以是单精度浮点数或双精度浮点数。默认是double类型
*/
float f = 3.14f; // 单精度浮点数
double d = 3.14; // 双精度浮点数
/**
* 字符字面量:表示单个字符的字面量,用单引号括起来。
*/
char ch = 'A'; // 字符'A'
/**
* 布尔字面量:表示真或假的字面量,只有两个值:true和false。
*/
boolean flag = true; // 布尔字面量
/**
* 字符串字面量:表示一串字符的字面量,用双引号括起来。
*/
String str = "Hello, World!"; // 字符串字面量
/**
* 空值:null。表示空,什么都没有。
*/
String string = null;
/**
* 常量值,又称字面常量
*/
final int MAX_VALUE = 100; // 常量值作为直接量
默认值
/**
* 默认值:
* 整数:0
* 浮点数:0.0
* 字符:'\u0000',即空字符
* 布尔:false
* 对象引用:null
*/
class MyDemo {
byte b;
short s;
int i ;
long l;
float f;
double d;
char c;
boolean bool;
String str;
public static void main(String[] args) {
MyDemo3 y = new MyDemo3();
System.out.println(y.b);
System.out.println(y.s);
System.out.println(y.i);
System.out.println(y.l);
System.out.println(y.f);
System.out.println(y.d);
System.out.println(y.c == '\u0000');
System.out.println(y.bool);
System.out.println(y.str);
}
}
常量相关知识
常量是在程序运行过程中其值不会发生改变的变量。在Java中,常量使用关键字final来定义。常量的定义格式为:
final 数据类型 常量名 = 值;
常量的特点包括:
1、不可改变性:常量一旦被赋值后,其值不能被修改。
2、可读性:常量的名称通常使用大写字母,多个单词间使用下划线分隔,以提高代码的可读性。
3、编译时确定:常量的值在编译时就已经确定,并且在运行时不会发生改变。
4、全局可访问:常量可以在程序的任何地方被访问,无需实例化对象。
/**
* 我们定义了两个常量MAX_VALUE和PI,分别表示一个整型的最大值和圆周率。
* 在main方法中,我们可以直接使用这些常量,并输出它们的值。
* 注意,尝试修改常量的值会导致编译错误,因为常量的值是不可变的。
*/
public class ConstantExample {
public static final int MAX_VALUE = 100; // 定义一个整型常量,表示最大值
public static final double PI = 3.14159; // 定义一个双精度浮点型常量,表示圆周率
public static void main(String[] args) {
System.out.println("最大值:" + MAX_VALUE);
System.out.println("圆周率:" + PI);
// 下面的代码会报错,因为常量的值不能被修改
// MAX_VALUE = 200;
// PI = 3.14;
}
}
变量相关知识
从程序设计来讲,变量是用来存储数据的内存空间,并且每个变量都有一个名称,以便在程序中引用和操作存储的数据,实现数据的存储、计算和修改。
因为 Java语言是强类型语言,变量必须先声明然后才能使用,并且需要指定变量的类型。
变量的分类:成员变量(静态变量、实例变量)、局部变量、参数变量
注意事项:变量不能重复声明、变量有自己的作用域和生命周期
变量的其他解释:
解释一:变量是一个数据储存空间的表示,它是储存数据的基本单元。
解释二:变量是在程序执行期间存储数据值的数据容器。
解释三:变量就是指计算机内存中一片存储区域,该区域有自己的名称(变量名)和自己的类型(数据类型),该区域存储的数据可以在同一类型范围内不断的变化
/**
变量分类:
成员变量:定义在方法体和语句块之外,不属于任何一个方法,作用域是整个类。
静态变量
修饰:用 static 修饰
访问:类名.变量名或对象名.变量名
生命周期:其生命周期取决于类的生命周期。类被垃圾回收机制彻底回收时才会被销毁
实例变量
修饰:无 static 修饰
访问:对象名.变量名
生命周期:只要对象被当作引用,实例变量就将存在
局部变量:局部变量是指在方法或者方法代码块中定义的变量,其作用域是其所在的代码块。
方法参数变量(形参):在整个方法内有效。
方法局部变量(方法内定义): 从定义这个变量开始到方法结束这一段时间内有效。
代码块局部变量(代码块内定义):从定义这个变量开始到代码块结束这一段时间内有效。
*/
/**
* 变量赋值:
* 格式一:先声明,后赋值。例如:数据类型 变量名;变量名 = 变量值;
* 格式二:声明时直接赋值。例如:数据类型 变量名 = 变量值;
*/
int age; // 声明一个整型变量
age = 20; // 初始化变量的值为20
int age2 = 23; //先声明一个int类型的变量,将数据23赋值给变量age
/**使用变量进行计算:*/
int num1 = 10;
int num2 = 5;
int sum = num1 + num2; // 使用变量进行加法运算
/**修改变量的值:*/
int count = 0;
count = 1; // 修改变量的值
/**
* 成员变量和局部变量的使用:
*/
public class MyDemo {
private String name; // 成员变量,实例变量
private static String username; // 成员变量,静态变量
public setName(String name) {// 形参
this.name = name; // 使用this关键字引用成员变量
}
public void printMessage() {
String message = "Hello, World!"; // 声明一个局部字符串变量
System.out.println(message); // 使用局部变量打印消息
}
}
数据类型相关知识
什么是数据类型、为什么要有数据类型、常见的数据类型有哪些、这些数据类型有什么用途、如何使用。
- 数据类型是编程语言中用来表示不同种类数据的分类或种类。它定义了数据的存储方式、操作规则以及可进行的操作。
- 数据类型在编程中起到了以下重要作用:内存分配和管理: 不同的数据类型需要不同的内存空间来存储数据。通过指定数据类型,编译器和运行时环境可以有效地管理内存分配和释放。数据操作和计算: 不同的数据类型支持不同的操作,例如数值计算、逻辑运算等。使用正确的数据类型可以确保数据操作的准确性和高效性。类型检查: 编译器可以在编译阶段检查类型错误,避免一些常见的编程错误。代码可读性和维护性: 明确的数据类型可以让代码更易读懂和维护,其他开发人员能够更好地理解代码的含义。
- 在Java中,数据类型分为两类:基本数据类型:(4类8种):
byte、short、int、long、float、double、char、boolean
。引用数据类型:类、接口、枚举、字符串、数组、集合、映射、…- 整数类型用于存储整数数据,如计数器、索引等。浮点数类型用于存储浮点数,如科学计算、货币计算等。字符类型用于存储单个字符,如文本处理。布尔类型用于表示逻辑真假值,如条件判断。类和接口用于定义对象的模板和行为。枚举用于定义一组常量,提高代码可读性。其他引用类型如字符串、数组、集合、映射用于存储和操作更复杂的数据结构。
- 在Java中,数据类型用于声明变量、方法参数、方法返回值等。
强类型语言相关知识
Java语言是强类型语言,强类型包含以下两方面的含义:
- 所有的变量必须先声明、后使用。声明变量类型就是要告诉计算机该变量的数据类型是什么,不同的类型在内存中分配不同的大小空间。
- 指定类型的变量只能接受类型与之匹配的值。对于每一种数据都定义了明确的类型,这意味着每个变量和每个表达式都有一个在编译时就确定的类型。类型限制了一个变量能被赋的值,限制了一个表达式可以产生的值,限制了在这些值上可以进行的操作,并确定了这些操作的含义。
Java中数据类型的分类:
基本数据类型:(4类8种):byte、short、int、long、float、double、char、boolean
引用数据类型:类、接口、枚举、字符串、数组、集合、映射、…
使用展示:
int i = 12;
float f = 3.14;
String str = "荒";
基础语法
表达式(赋值、算术、关系(比较)、逻辑、位运算、条件)
表达式相关知识
表达式:用于表达某种结果的式子,大部分是由符号拼接变量组合而成的,也有一小部分有符号拼接方法体组成。
表达式中使用的符号称为运算符。可分为:算术、赋值、比较(关系)、条件、逻辑 。
根据符号类型的不同可以大致分为:算术表达式、赋值表达式、条件表达式、逻辑表达式、lambda表达式。
赋值表达式
运算符:赋值
=
使用示例:dataType name = value
; 其中value的值可以是字面量,也可以是表达式的值。
在 Java 语言中,dataType 和 value 的类型必须匹配,如果类型不匹配则需要自动转化为对应的类型。
赋值运算符还可与其他运算符结合,扩展成功能更加强大的赋值运算符。
class Demo {
public static void main(String[] args) {
// 简单的赋值
double price = 10.25; // 定义商品的单价,赋值为10.25
int count = 2; // 定义购买数量,赋值为2
double total = 0; // 定义总价初始为0
total = price * count; // 总价=当前单价*数量
// 控制台打印
System.out.println("当前单价:" + price + ",当前数量:" + count + ",当前总价:" + total);
// 算术赋值
price -= 1.25; // 减去降价得到当前单价
count *= 5; // 现在需要购买10个,即原来数量的5倍
total = price * count; // 总价=当前单价*数量
// 控制台打印
System.out.println("当前单价:" + price + ",当前数量:" + count + ",当前总价:" + total);
}
}
算术表达式
运算符:取反
-
、自加++
、自减--
、加+
、减-
、乘*
、除/
、取余%
、加赋值+=
、减赋值-=
、乘赋值*=
、除赋值/=
、取余赋值%=
。大致可分为三类:一元运算:
取反(-):取反运算 例如:-a
自加(++):先取值再加一,或先加一再取值 例如:a++、++a
自减(–):先取值再减一,或先减一再取值 例如:a–、–a二元运算:
加(+):求两数之和,以及用于字符串连接操作 例如:a + b
减(-):求a减b的差 例如:a - b
乘(*):求a乘以b的积 例如:a * b
除(/):求a除以b的商 例如:a / b
取余(%):求a除以b的余数 例如:a % b算术赋值运算:
加赋值(+=):将该运算符左边的数值加上右边的数值, 其结果赋值给左边变量本身 例子:a += b、a += b+3
减赋值(-=):将该运算符左边的数值减去右边的数值, 其结果赋值给左边变量本身 例子:a -= b
乘赋值(*=):将该运算符左边的数值乘以右边的数值, 其结果赋值给左边变量本身 例子:a *= b
除赋值(/=):将该运算符左边的数值整除右边的数值, 其结果赋值给左边变量本身 例子:a /= b
取余赋值(%=):将该运算符左边的数值除以右边的数值后取余,其结果赋值给左边变量本身 例子:a %= b
class Demo1 {
public static void main(String[] args) {
int a = 12, b = 12, c = 12;
int d = -a, e = --b, f = c--;
System.out.println("a:" + a + ",b:" + b + ",c:" + c);// a:12,b:11,c:11
System.out.println("d:" + d + ",e:" + e + ",f:" + f); // d:-12,e:11,f:12
// 结论:靠近等号,先执行赋值操作。靠近自增自减符号,先执行自增自减操作,然后把结果赋值。
int g = 4, h = 2, i = 3;
int j = g * (g + h) % i;
// 先执行(g + h)=6,在执行 g * 6 = 24,最后执行24 % 3 = 0
System.out.println(a += b);// 相当于 a = a + b
System.out.println(a += b + 3);// 相当于 a = a + b + 3
System.out.println(a -= b);// 相当于 a = a - b
System.out.println(a *= b);// 相当于 a=a*b
System.out.println(a /= b);// 相当于 a=a/b
System.out.println(a %= b);// 相当于 a=a%b
}
}
关系表达式
关系运算符(relational operators)也可以称为“比较运算符”,用于用来比较判断两个变量或常量的大小。关系运算符是二元运算符,运算结果是 boolean 型。当运算符对应的关系成立时,运算结果是 true,否则是 false。
运算符:大于>
、小于<
、等于==
、大于等于>=
、小于等于<=
、不等于!=
。== 和 != 可以应用于基本数据类型和引用类型。当用于引用类型比较时,比较的是两个引用是否指向同一个对象,但当时实际开发过程多数情况下,只是比较对象的内容是否相当,不需要比较是否为同一个对象。
注意点:
只能同种类型才可以使用 ==或者 != 进行比较;
只有基本类型才能使用不等式进行比较;
boolean 类型只能使用 ==或者 != 进行比较;大于(>):只支持左右两边操作数是数值类型。如果前面变量的值大于后面变量的值, 则返回 true。
大于或等于(>=):只支持左右两边操作数是数值类型。如果前面变量的值大于等于后面变量的值, 则返回 true。
小于(<):只支持左右两边操作数是数值类型。如果前面变量的值小于后面变量的值,则返回 true。
小于或等于(<=):只支持左右两边操作数是数值类型。如果前面变量的值小于等于后面变量的值, 则返回 true。
相等(==):如果进行比较的两个操作数都是数值类型,无论它们的数据类型是否相同,只要它们的值相等,也都将返回 true。如果两个操作数都是引用类型,只有当两个引用变量的类型具有父子关系时才可以比较,只要两个引用指向的不是同一个对象就会返回 true。
不相等(!=):如果进行比较的两个操作数都是数值类型,无论它们的数据类型是否相同,只要它们的值不相等,也都将返回 true。如果两个操作数都是引用类型,只有当两个引用变量的类型具有父子关系时才可以比较,只要两个引用指向的不是同一个对象就会返回 true。由于计算机内存放的实数与实际的实数存在着一定的误差,如果对浮点数进行 ==(相等)或 !=(不相等)的比较,容易产生错误结果,应该尽量避免。
逻辑表达式
逻辑运算符:短路逻辑运算:
&& ||
、非短路逻辑运算:& |
逻辑运算符是对布尔型变量进行运算,其结果也是布尔型。逻辑运算符把各个运算的关系表达式连接起来组成一个复杂的逻辑表达式,以判断程序中的表达式是否成立,判断的结果是 true 或 false。
短路与运算&&:如果布尔表达式1&&布尔表达式2,一旦表达式1的值是false,那么不计算表达式2的值,整体返回false
短路或运算||:如果布尔表达式1||布尔表达式2,一旦表达式1的值为true,那么不计算表达式2的值,整体返回true
位运算表达式
Java 定义的位运算(bitwise operators)直接对整数类型的位进行操作,这些整数类型包括 long,int,short,char 和 byte。
位运算符主要用来对操作数二进制的位进行运算。按位运算表示按每个二进制位(bit)进行计算,其操作数和运算结果都是整型值。
Java 语言中的位运算符分为位逻辑运算符和位移运算符两类位逻辑运算符
&(与):按位进行与运算(AND)
|(或):按位进行或运算(OR)
^(异或)。按位进行异或运算(XOR)
~(非,即位取反):按位进行取反运算(NOT)位移运算符
位移运算符用来将操作数向某个方向(向左或者右)移动指定的二进制位数。复合位赋值运算符
所有的二进制位运算符都有一种将赋值与位运算组合在一起的简写形式。复合位赋值运算符由赋值运算符与位逻辑运算符和位移运算符组合而成。
&= 按位与赋值
|= 按位或赋值
^= 按位异或赋值
~= 按位取反赋值
«= 按位左移赋值
»= 按位右移赋值位与运算符
位与运算符为&,其运算规则是:参与运算的数字,低位对齐,高位不足的补零,如果对应的二进制位同时为 1,那么计算结果才为 1,否则为 0。因此,任何数与 0 进行按位与运算,其结果都为 0。位或运算符
位或运算符为|,其运算规则是:参与运算的数字,低位对齐,高位不足的补零。如果对应的二进制位只要有一个为 1,那么结果就为 1;如果对应的二进制位都为 0,结果才为 0。位异或运算符
位异或运算符为^,其运算规则是:参与运算的数字,低位对齐,高位不足的补零,如果对应的二进制位相同(同时为 0 或同时为 1)时,结果为 0;如果对应的二进制位不相同,结果则为 1。
提示:在有的高级语言中,将运算符^作为求幂运算符,要注意区分。位取反运算符
位取反运算符为~,其运算规则是:只对一个操作数进行运算,将操作数二进制中的 1 改为 0,0 改为 1。左位移运算符
左移位运算符为«,其运算规则是:按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。右位移运算符
右位移运算符为»,其运算规则是:按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的空位补零。
条件表达式
条件运算符的符号表示为“?:”,使用该运算符时需要有三个操作数,因此称其为三目运算符。
语法结构为:
result = ? : ;
其中,expression 是一个布尔表达式。当 expression 为真时,执行 statement1, 否则就执行 statement3。此三元运算符要求返回一个结果,因此要实现简单的二分支程序,即可使用该条件运算符。可以将条件运算符理解为 if-else 语句的简化形式。在使用条件运算符时,还应该注意优先级问题,例如下面的表达式:x>y ? x-=y : x+=y; 等价于 (x>y ? x-=y : x)+=y; 正确的写法:(x>y) ? (x-=y) : (x+=y); 因为条件运算符优先于赋值运算符
运算符(赋值、算术、关系(比较)、逻辑、位运算、条件)
java运算符优先级
先括号、在乘除、后加减、其次关系,再次逻辑、最后赋值
运算符相关内容
待添加
流程(顺序(默认)、选择(分支:if、switch)、循环(for、while)、结束(break、continue、return))
流程相关知识
完成一个完整的业务行为的过程,可称之为流程。从结构化程序设计角度划分有三种结构:顺序结构、选择结构和循环结构。
顺序结构是系统默认的执行流程。自上而下一行一行地执行该程序。选择结构的出现是因为顺序结构不能判断。可以根据一个条件判断执行哪些语句块。而循环结构的出现是解决程序中大量的重复性的操作。循环语句能够使程序代码重复执行,适用于需要重复一段代码直到满足特定条件为止的情况。
选择语句相关知识
Java支持两种选择语句:if 语句和 switch 语句。其中 if 语句使用布尔表达式或布尔值作为分支条件来进行分支控制,而 switch 语句则用于对多个整型值进行匹配,从而实现分支控制。
if…else 语句可以用来描述一个“二岔路口”,我们只能选择其中一条路来继续走,然而生活中经常会碰到“多岔路口”的情况。switch 语句提供了 if 语句的一个变通形式,可以从多个语句块中选择其中的一个执行。
if语句相关知识
if 语句是使用最多的条件分支结构,它属于选择语句,也可以称为条件语句。
if语句结构格式: 如果条件为真,执行语句块;如果条件为假,则不执行语句块。
if(条件表达式){语句块;}
**if-else语句结构格式:**如果条件为真,执行语句块1;如果条件为假,则执行语句块2。
if (表达式) {语句块1;} else {语句块2;}
**if-else-if语句结构格式:**执行性表达式为真的语句块;如果所有表达式都为假,执行else下的语句块
if (表达式1) {语句块1;} else if(表达式2){语句块2;} else{语句块3;}
多重if语句嵌套:
if(表达式1){ if(表达式2){语句块1;}else{语句块2;} }else{ if(表达式3){语句块3;}else{语句块4;} }
switch语句相关知识
switch 语句是 Java 的多路分支语句。它提供了一种基于一个表达式的值来使程序执行不同部分的简单方法。
switch语法格式:
switch(表达式) { case 值1: 语句块1; break; case 值2: 语句块2; break; ……… default: 语句块n+1; break; }
多重switch嵌套语句:
注意事项:
switch表达式的数据类型是基本类型和String类型,case标签后面跟着常量或字面量,break标签表示停止,跳出当前结构(跳出switch语句),default标签表示所有条件都不满足时才可以执行语句块,其位置随意。
循环语句相关知识
Java 支持四种循环语句:while语句、do-while语句、for语句、for-each语句,其中for-each 循环是 for 循环的变形,它是专门为集合遍历而设计的。
循环是程序中的重要流程结构之一。循环语句能够使程序代码重复执行,适用于需要重复一段代码直到满足特定条件为止的情况。
循环语句基本组成:
初始化语句:初始化语句在循环开始之前执行
循环条件:boolean表达式,其值能决定是否执行循环体
循环体:循环的主体、被重复执行的代码块
迭代语句:在一次循环体执行结束后,对循环条件求值之前执行,控制循环条件中的变量,使得循环在合适的时候结束。
while语句相关知识
while语句格式:
while(条件表达式) {语句块;}
注意事项:先判断,如果条件为真,则执行代码块
do-while语句相关知识
do-while语句格式:
do {语句块;}while(条件表达式);
注意事项:先执行循环体,然后判断循环条件是否成立,成立则再次执行代码块(至少执行一次)。
for语句相关知识、
for 语句是应用最广泛、功能最强的一种循环语句。大部分情况下,for 循环可以代替 while 循环、do while 循环。
for语句格式:
for(条件表达式1;条件表达式2;条件表达式3) {语句块;}
条件表达式1 赋值语句 循环结构的初始部分,为循环变量赋初值
条件表达式2 条件语句 作为循环结构的循环条件
条件表达式3 迭代语句 通常使用 ++ 或 – 运算符,用来修改循环变量的值注意事项: for语句通常使用在知道循环次数的循环中。for语句中初始化、循环条件以及迭代部分都可以为空语句(但分号不能省略),三者均为空的时候,相当于一个无限循环。
双重for循环语句格式:
for(条件表达式1;条件表达式2;条件表达式3) { for(条件表达式1;条件表达式2;条件表达式3) { 语句块; } }
for-each语句相关知识
for-each语法格式:
for(类型 变量名:集合) {语句块;}
“类型”为集合元素的类型。“变量名”表示集合中的每一个元素。“集合”是被遍历的集合对象或数组。
每执行一次循环语句,循环变量就读取集合中的一个元素。
break、continue、return三者区别
break、continue、return三者都可用于结束循环。
break语句的作用是终止循环,仅限于break所在的循环语句。
continue语句的作用是终止本次循环,不在执行后续的代码了。
return语句的作用终止当前函数的执行,将控制权返回该方法的调用者。
面向对象(类(创建类、类中关键词、内部类)、对象(创建、使用、三大特性))
面向对象
- 面向对象相关知识、类和对象的相关知识、
- 创建类、类中的关键词详解、内部类详解、方法详解、构造器详解、封装继承多态详解。
- 创建对象
面向对象相关知识
Java 是面向对象的编程语言,面向怎么理解,可以理解成围绕,围绕着对象进行编程。对象怎么理解,对象可以看作是真实世界的各种实体。面向对象,可以理解成围绕着现实世界中各种各样的实体进行编程。
将同种对象归纳总结出其具有的基本特征和行为,将其模板化,这个模板就是类。简单来说:类就是对象模板
面向对象开发是相对面向过程而言的,面向对象程序设计有以下优点:可重用,可扩展、可管理。这源于面向对象的三个核心特性:封装、继承、多态。
类和对象的相关知识
将对象进行模板化就会形成类。类是对某一类事物的描述(属性、行为),而对象就是该类事物的实例,是具体的实体,有着明确的属性和行为。例如老鼠,这是统称,是一个类。例如花枝鼠,这也是统称,也是一个类。例如一只名叫棉花糖的花枝鼠,有着明确的属性,这是花枝鼠这个类中的一个实例,是一个对象。
在Java中,所有的 Java 程序都是基于类的,是组成 Java 程序的基本要素,也是是 Java 中的一种重要的引用数据类型。
类(创建类、类中关键词、内部类)
创建类的语法格式
在Java中,定义一个类,需要使用 class 关键字、一个自定义的类名和一对表示程序体的大括号。
简单的语法格式:class 类名 { 属性、方法 }
。
完整的语法格式:[修饰词1] class 类名1 [修饰词2 类名2] { 属性、方法 }
。
类中的数据和方法统称为类成员。通过在类的主体中定义变量来描述类所具有的特征(属性),这里声明的变量称为类的成员变量。类的方法描述了类所具有的行为,是类的成员方法。可以简单地把方法理解为独立完成某个功能的单元模块。在Java中,存在着接口、枚举这两种数据类型。他们不属于类,但都可以被看作是一种特殊类型的类。在Java中,类、接口和枚举都是面向对象编程的基本构建块,用于表示对象和定义行为。
/**
* 中括号“[]”中的部分表示可以省略,竖线“|”表示“或关系”。
* 例如 abstract|final,说明可以使用 abstract 或 final 关键字,但是两个关键字不能同时出现。
*/
[public][abstract|final] class <class_name>[extends <class_name> ][implements<interface_name>] {
// 定义属性部分
<property_type><property1>;
<property_type><property2>;
<property_type><property3>;
…
// 定义方法部分
function1();
function2();
function3();
…
}
创建接口的语法格式
接口是一种完全抽象的类,其中只包含方法的声明而没有方法的实现。它定义了一组规范,供其他类实现。
在Java中,定义一个接口,需要使用 interface 关键字、一个自定义的接口名和一对表示程序体的大括号。
简单的语法格式:interface 接口名 { 属性、方法 }
。
完整的语法格式:[public] interface 接口名 [extends 父接口1, 父接口2, ...] { 属性、方法 }
。
创建枚举的语法格式
枚举是一种特殊的类,用于表示一组固定的常量。枚举类可以包含方法和其他成员变量,但它的实例是有限的、固定的。每个枚举值都是该枚举类型的一个实例,可以包含字段、方法
在Java中,定义一个枚举,需要使用 enum 关键字、一个自定义的枚举名和一对表示程序体的大括号。
简单的语法格式:enum 枚举名 { 枚举值列表、属性、方法、构造器}
。
完整的语法格式:[public] enum 枚举名 [implements 父接口1,...]{ 枚举值列表、属性、方法、构造器 }
。
创建类的实际案例
创建一个新的类,就是创建一个新的数据类型。实例化一个类,就是得到类的一个对象。因此,对象就是一组变量和相关方法的集合,其中变量表明对象的状态和属性,方法表明对象所具有的行为。
类、抽象类、接口、枚举,这四个都可以看作是类。创建步骤如下:
(1) 声明类。编写类的最外层框架,public class Demo{ 类的主体 }
。
(2) 编写类的属性。类中的数据和方法统称为类成员。其中,类的属性就是类的数据成员。通过在类的主体中定义变量来描述类所具有的特征(属性),这里声明的变量称为类的成员变量。
(3) 编写类的方法。类的方法描述了类所具有的行为,是类的方法成员。可以简单地把方法理解为独立完成某个功能的单元模块。
public class Demo999 {
public static void main(String[] args) {
// 创建花枝鼠对象
System.out.println("********花枝鼠对象");
FlowerBranchRat HZS = new FlowerBranchRat();
System.out.println(HZS.name + ",我的其中一个名称叫" + HZS.nickName);
HZS.action();
HZS.age();
HZS.show();
HZS.show("奶牛大白鼠");
// 创建老鼠对象
System.out.println("********老鼠对象");
Rat rat = new FlowerBranchRat();
System.out.println(rat.name);
rat.action();
rat.show();
// 创建猫猫对象
System.out.println("********猫猫对象");
Cat cat = new Cat();
cat.action();
cat.show();
// 创建动物对象
System.out.println("********动物对象");
Animal rat1 = new Rat();
rat1.show();
Animal HZS1 = new FlowerBranchRat();
HZS1.show();
Animal cat1 = new Cat();
cat1.show();
// 创建生物对象
System.out.println("********生物对象");
Organism organism = new FlowerBranchRat();
organism.age();
// 使用枚举
System.out.println("********角色");
System.out.println(Role.CAT.name());
System.out.println(Role.CAT.getName());
}
}
/**
* 生物(接口):
*/
interface Organism {
void age();
}
/**
* 角色(枚举)
*/
enum Role {
CAT(110, "黑猫警长"),
RAT(500, "一只耳");
private final int code;
private final String name;
public int getCode() {
return code;
}
public String getName() {
return name;
}
Role(int code, String name) {
this.code = code;
this.name = name;
}
}
/**
* 动物(抽象类):
*/
abstract class Animal {
abstract void show();
}
/**
* 猫
*/
final class Cat extends Animal {
public void action() {
System.out.println("捉老鼠");
}
@Override
void show() {
System.out.println("猫猫我啊继承了动物抽象类Animal");
}
}
/**
* 老鼠(父类)
*/
class Rat extends Animal {
public String name = "老鼠";
public void action() {
System.out.println("老鼠吃大米");
}
@Override
public void show() {
System.out.println("老鼠我啊继承了动物抽象类Animal");
}
}
/**
* 花枝鼠(子类):
*/
class FlowerBranchRat extends Rat implements Organism {
// public String name = "老鼠"; // 继承父类属性
public String nickName = "花枝鼠";
public void action() {
System.out.println("花枝鼠吃大米");
}
@Override
public void age() {
System.out.println("棉花糖我啊只能活两三年");
}
// 继承父类方法
// public void show() {
// System.out.println("老鼠我啊继承了动物抽象类Animal");
// }
// 子类重载父类方法
public void show() {
System.out.println("棉花糖我啊继承了动物抽象类Animal");
}
// 重写类中方法
public void show(String name) {
System.out.println(name + "我啊继承了动物抽象类Animal");
}
}
类中的关键词详解
在创建类的同时,接触到了诸多修饰符:public、private、abstract、static、final、extends、implements
public、private属于访问控制修饰符,其作用域各不相同。abstract、static、final这三种不知道怎么划分。extends、implements属于类之间的关系修饰符。abstract用于修饰抽象类和抽象方法。static用于修饰类中的属性、方法、代码块以及修饰内部类。final用于修饰类、变量和方法。
访问控制修饰符相关知识
在实际开发中,存在着可以随意访问和修改对象的属性的缺陷。所以需要使用访问修饰符来进行信息隐藏,避免核心信息被用户调用。
对类成员访问的限制是面向对象程序设计的一个基础,这有利于防止对象的误用。只允许通过一系列定义完善的方法来访问私有数据,就可以(通过执行范围检查)防止数据赋予不正当的值。例如,类以外的代码不可能直接向一个私有成员赋值。同时,还可以精确地控制如何以及何时使用对象中的数据。
public class Demo {
public static void main(String[] args) {
Rat1 rat1 = new Rat1();
// rat1对象无法调用其属性,只能通过set方法赋值,get方法取值
// 如果没有set方法、get方法,对象信息将无法修改、无法显示,只能传递。
rat1.setName("大耗子");
System.out.println(rat1.getName());
}
}
class Rat1 {
private String name;
private int age;
public int getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
}
访问控制修饰符的可见范围
修饰类修饰属性修饰方法的访问可见范围
公有的public
:任意位置可见。
受保护的protected
:在同一个包中可见以及子类可见。
默认的default
:在同一个包中可见,默认就是不加修饰符。
私有的private
:仅类内部可见。
/**
* 类修饰符:public、不加修饰符
* 变量修饰符:private、不加修饰符、protected、public
* 方法修饰符:private、不加修饰符、protected、public
*/
public class Demo {
private String name1;
String name2
protected String name3;
public String name4;
}
class Demo1 {
private void fun1(){}
void fun2(){}
protected void fun3(){}
public void fun4(){}
}
abstract 修饰符相关知识
static 修饰符相关知识
在Java中,static修饰符可用于修饰类中的属性、方法、代码块以及修饰内部类。其中修饰的属性(成员变量)称为静态变量,也可以称为类变量,修饰的常量称为静态常量,修饰的方法称为静态方法或类方法,修饰的代码块称为静态代码块,修饰的内部类称为静态内部类,它们统称为静态成员,归整个类所有。
静态成员不依赖于类的特定实例,被类的所有实例共享,也就是说静态成员不需要依赖于对象来进行访问,只要这个类被加载(Java程序中存在不被加载的类),Java虚拟机就可以根据类名找到它们。
调用静态成员的语法形式如下:
类名.静态成员
注意:类中的属性、方法都属于类成员。static 修饰的成员变量和方法,从属于类,属于静态成员。普通变量和方法从属于对象,属于实例成员。静态方法不能调用非静态成员,编译会报错。
/**
* 类中的属性、方法都属于类成员。
* static修饰过的属于静态成员
* 没有static修饰的属于实例成员或者非静态成员
*/
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
}
// 静态变量
static String name;
// 静态常量
static final String NAME = "棉花糖";
// 静态方法
static void fun(){
System.out.println("静态方法");
}
// 静态代码块
static {
System.out.println("静态代码块");
}
// 静态内部类
static class Demo1{
public void fun(){
System.out.println("静态内部类");
}
}
// 实例变量
String name2;
// 实例常量
final String NAME2 = "棉花糖2";
// 实例方法
void fun2(){
System.out.println("实例方法");
}
// 非静态代码块
{
System.out.println("非静态代码块");
}
// 非静态内部类
class Demo2{
public void fun(){
System.out.println("非静态内部类");
}
}
}
Java程序中存在不被加载的类的原因
在Java中,类的加载是指将类的字节码文件加载到内存中,并创建相应的Class对象的过程。但并不是所有的类都会被加载到内存中,只有在程序运行时需要使用到的类才会被加载。
以下是一些情况下可能存在不被加载的类:
未被引用的类:如果一个类在程序中没有被引用或使用,那么它就不会被加载。这通常发生在没有被调用的类或者未被引用的类。
条件未满足的类:有些类可能只在特定的条件下才会被加载。例如,当程序运行到某个分支时才会加载该分支下的类。
被优化未使用的类:Java虚拟机(JVM)在运行过程中可能会对类进行优化,包括将未使用的类从加载中排除。
需要注意的是,即使一个类没有被直接引用,但可能会被Java虚拟机(JVM)的类加载器预加载。这是因为JVM可能会预加载一些类,以提高程序的启动性能或为后续的类加载做准备。
总而言之,在Java程序中存在不被加载的类是可能的,这取决于程序的设计和运行时的需求。
import static静态导入
在 JDK 1.5 之后增加了一种静态导入的语法,用于导入指定类中的静态成员变量、方法。如果一个类中存在使用 static 声明的静态方法,则在导入时就可以直接使用 import static 的方式导入。其语法如下:
import static package.ClassName.xxx;
导入指定类的指定静态成员变量、方法
import static package.ClassName.*;
导入指定类的全部静态成员变量、方法import 和 import static 的作用:使用 import 可以省略包名(导入指定包中的类,可以直接使用类),使用 import static 可以省略类名(导入指定包中的类中的方法,可以直接使用方法)。
/**
* 类中存在静态方法
*/
package demo.utils.test2;
import java.util.Arrays;
public class Demo {
public static void print(){
/**
* java.util.List,指定包中的类,不用加 import java.util.List;
* Arrays,导入指定包中的类,需要加 import java.util.Arrays;
*/
java.util.List<Integer> integers = Arrays.asList(1, 2, 3);
System.out.println(integers);
}
public void fun(){
System.out.println("fun");
}
public void action(){
System.out.println("action");
}
}
/**
* 调用test2包下的Demo中的静态方法
*/
package demo.utils.test1;
// import static demo.utils.test2.Demo.print;
import static demo.utils.test2.Demo.*;
public class Demo {
public static void main(String[] args) {
print();
}
}
final修饰符相关知识
使用 final 关键字声明类、变量和方法需要注意以下几点:
final修饰变量,其值不可改变,该变量此时可以称为常量。
final修饰方法,其方法不可被重写,即子类不允许重写父类使用final修饰的方法。
final修饰类,其类不可以被继承,即final修饰的类不允许有子类。final 在Java中的表示最终的。最终的变量、最终的方法、最终的类。原因吗:一切为了安全,不允许改变。
使用 final 声明变量时,要求全部的字母大写
class Demo1{
final int i = 90;
final void action(){
// i = 80; // 常量不允许重新赋值
System.out.println("final");
}
}
final class DemoA extends Demo1{
// 不允许重写方法(覆盖方法)
// void action(){
// System.out.println("final");
// }
}
// final修饰的类不允许被继承(不可以有子类)
class DemoB /*extends DemoA*/ {
}
final 修饰变量
final 修饰的变量即成为常量,只能赋值一次。其中有两个注意点:final 修饰的局部变量和成员变量有所区别。final 修饰的基本类型变量和引用类型变量有所区别。
- final 修饰的局部变量必须使用之前被赋值一次才能使用。
- final 修饰的成员变量在声明时没有赋值的叫“空白 final 变量”。空白 final 变量必须在构造方法或静态代码块中初始化。
- final 修饰基本类型变量时,不能重新赋值,其值不可改变。这是因为基本类型变量存储的是实际的数据值,在内存中已经分配了特定的空间来存储该变量的值。
- final 修饰引用类型变量时,指向的是对象的引用,保存的是该对象的引用地址,final只能保证这个引用类型变量所引用的对象地址不会发生改变,即一直引用同一个对象。因此这个对象的内容并不被限制,即内容可以发生变化。
- 注意:final 修饰的变量不能被赋值这种说法是错误的,严格的说法是,final 修饰的变量不可被改变,一旦获得了初始值(被初始化),该 final 变量的值就不能被重新赋值。
final修饰基本类型变量
class FinalDemo {
// 实例常量
final int a = 100; // 直接赋值
final int b; // 空白final变量,只能由创建对象时赋值了
FinalDemo() {
b = 200; // 初始化实例变量
// b = 200; // 二次赋值,编译报错
}
// 静态常量
final static int c = 300;// 直接赋值
final static int d; // 空白final变量,只能加载类的时候赋值了
static {
d = 400; // 初始化静态变量
// d = 400; // 二次赋值,编译报错
}
void action() {
final int e = 500; // 直接赋值
final int f; // 空白final变量
f = 600;
// f =700; // 二次赋值,编译报错
}
}
final修饰引用类型变量
class FinalDemo2 {
public static void main(String[] args) {
// final修饰引用类型变量时,不能重新赋值(引用地址不可变),可以修改对象的内容(引用内容可变)。
final List<Integer> in = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); // 返回一个可变集合
// in = new ArrayList<>(); // 二次赋值,编译报错
in.add(6); // 修改对象内容,正常编译
}
}
final 修饰方法
final 修饰的方法不可被重写,如果出于某些原因,不希望子类重写父类的某个方法,则可以使用 final 修饰该方法,通过使用 final 把这个方法密封起来。
使用private修饰的方法,子类无法重写。所以子类定义了一个和父类 private 方法有相同方法名、相同形参列表、相同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。因此子类可以定义一个父类使用final 修饰的private 访问权限的方法。
final 修饰的方法仅仅是不能被重写。final 修饰的方法可以被重载。
class FinalDemo3 {
public static void main(String[] args) {
FinalDemo4 f = new FinalDemo5();
// f.name(); // 在当前类中,无法调用FinalDemo4的私有方法
f.action();
f.action("重载");
}
}
class FinalDemo4 {
public final void action() {
System.out.println("父类");
}
// private修饰的方法仅在当前类中可见,其他任何类都无法调用
private final void name(String name) {
System.out.println(name + ",父类");
}
// final 修饰的方法只是不能被重写,完全可以被重载
public final void action(String name) {
System.out.println("方法重载" + name);
}
}
class FinalDemo5 extends FinalDemo4 {
// 编译报错,final修饰的方法不允许重写
// public void action(){
// System.out.println("子类");
// }
// private修饰的方法仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法
// 所以定义一个和父类相同的方法,只是重新定义了一个新方法,不属于方法重写
public void name(String name) {
System.out.println(name + ",子类");
}
}
final 修饰类
final 修饰的类不能被继承。当子类继承父类时,将可以访问到父类内部数据,并可通过重写父类方法来改变父类方法的实现细节,这可能导致一些不安全的因素。为了保证某个类不可被继承,则可以使用 final 修饰这个类。
final 修饰符使用总结
- final 修饰类中的变量
表示该变量一旦被初始化便不可改变,这里不可改变的意思对基本类型变量来说是其值不可变,而对对象引用类型变量来说其引用不可再变。其初始化可以在两个地方:一是其定义处,也就是说在 final 变量定义时直接给其赋值;二是在构造方法中。这两个地方只能选其一,要么在定义时给值,要么在构造方法中给值,不能同时既在定义时赋值,又在构造方法中赋予另外的值。- final 修饰类中的方法
说明这种方法提供的功能已经满足当前要求,不需要进行扩展,并且也不允许任何从此类继承的类来重写这种方法,但是继承仍然可以继承这个方法,也就是说可以直接使用。在声明类中,一个 final 方法只被实现一次。- final 修饰类
表示该类是无法被任何其他类继承的,意味着此类在一个继承树中是一个叶子类,并且此类的设计已被认为很完美而不需要进行修改或扩展。对于 final 类中的成员,可以定义其为 final,也可以不是 final。而对于方法,由于所属类为 final 的关系,自然也就成了 final 型。也可以明确地给 final 类中的方法加上一个 final,这显然没有意义。
extends 关系修饰符相关知识
implements 关系修饰符相关知识
内部类详解
- 内部类如何理解:在一个类的内部定义了一个类,里面的这个类称为内部类,也称为嵌套类,外面的类被称为外部类。如果有多层嵌套,通常将最外层的类称为顶层类(或者顶级类)。类比于循环,循环结构的内部又使用了一个循环结构,里面的循环叫内部循环,外面的循环叫外部循环。又类似于分支,选择结构的内部嵌套了新的选择结构,if语句里面嵌套了if语句。
- 内部类可分为哪几种:静态内部类(使用static修饰)、实例内部类(没有static修饰)、局部内部类(定义在方法中)、匿名内部类()
- 内部类诞生的原由:实现回调机制、实现多重继承、减少代码量
- 内部类的特点:可以自由地访问外部类的成员变量、静态内部类只能访问外部类的静态成员变量。
- 内部类有 4 种访问级别:public、protected、 private 和默认。外部类则只有两种访问级别:public 和默认;
- 内部类对象创建语法格式:
- 在外部类中创建成员内部类对象语法:内部类 对象名 = new 内部类();。
- 在外部类以外的其他类中创建成员内部类对象语法:内部类 对象名 = new 外部类().new 内部类();
提示:内部类的很多访问规则可以参考变量和方法。另外使用内部类可以使程序结构变得紧凑,但是却在一定程度上破坏了Java 面向对象的思想。
class OuterDemo { // 外部类
class InnerDemo1 { } // 实例内部类
static class InnerDemo2 { } // 静态内部类
void action() {
class InnerDemo3 { } // 局部内部类
}
OuterDemo out = new OuterDemo(){}; // 匿名内部类
}
实例内部类(没有static修饰)
实例内部类是指没有用 static 修饰的内部类,有的地方也称为非静态内部类。
在实例内部类中,可以访问外部类的所有成员。提示:如果有多层嵌套,则内部类可以访问所有外部类的成员。
在外部类中不能直接访问内部类的成员,而必须通过内部类的实例去访问。如果类 A 包含内部类 B,类 B 中包含内部类 C,则在类 A 中不能直接访问类 C,而应该通过类 B 的实例去访问类 C。
外部类实例与内部类实例是一对多的关系,也就是说一个内部类实例只对应一个外部类实例,而一个外部类实例则可以对应多个内部类实例。
如果实例内部类 B 与外部类 A 包含有同名的成员 t,则在类 B 中 t 和 this.t 都表示 B 中的成员 t,而 A.this.t 表示 A 中的成员 t。
在实例内部类中不能定义 static 成员,除非同时使用 final 和 static 修饰。
在不同位置创建实例内部类对象的区别
在外部类内部的静态属性上以及静态方法中,必须通过外部类的实例创建内部类的实例。
在外部类以外的其他类中,必须通过外部类的实例创建内部类的实例
其余的内部类实例创建同寻常的类实例创建简单点来说:在外部类的静态方法和外部类以外的其他类中,必须通过外部类的实例创建内部类的实例
class OuterDemo1 { // 外部类
class InnerDemo1 { } // 实例内部类
/**
* 在外部类中创建内部类的实例:即在成员中创建内部类实例
* 1、静态变量位置上创建内部类实例,需要创建外部类实例
* 2、实例变量位置上创建内部类实例,直接创建
* 3、静态方法中创建内部类实例,需要创建外部类实例
* 4、实例方法中创建内部类实例,直接创建
* 5、其他内部类中创建内部类实例,直接创建
*/
static InnerDemo1 in = new OuterDemo1().new InnerDemo1();
InnerDemo1 in2 = new InnerDemo1();
public static void method2() {
InnerDemo1 in = new OuterDemo1().new InnerDemo1();
}
public void action() {
InnerDemo1 in = new InnerDemo1();
}
class InnerDemo2 {
InnerDemo1 in = new InnerDemo1();
}
}
/**
* 在外部类以外的类中创建内部类实例,需要创建外部类实例
*/
class OtherDemo {
OuterDemo1.InnerDemo1 in = new OuterDemo1().new InnerDemo1();
}
在内部类中访问外部类的成员
在实例内部类中,可以访问外部类的所有成员。如果有多层嵌套,则内部类可以访问所有外部类的成员。
public class OuterDemo2 { // 外部类
// 四种访问权限:private、default、protected、public
private int a = 1;
int b = 2;
protected int c = 3;
public int d = 4;
// 两种状态:静态、常量
static int e = 5;
final int f = 6;
// 无返回值实例方法、有返回值实例方法、有返回值静态方法
void action() {
System.out.println("无返回值的实例方法");
}
String action1() { // 可以为变量赋值
return "实例方法";
}
static String action2() { // 可以为变量赋值
return "静态方法";
}
public class InnerDemo2 {
// 访问外部类中所有的成员(属性和方法)
int a1 = a + 1;
int b1 = b + 1;
int c1 = c + 1;
int d1 = d + 1;
int e1 = e + 1;
int f1 = f + 1;
String str = action1(); // 有返回值的方法可以为变量赋值
String str1 = action2();
String fun() {
action(); // 无返回值的方法可以在方法中调用
return action1() + action2();
}
}
}
在外部类中访问内部类的成员
- 在外部类中不能直接访问内部类的成员,而必须通过内部类的实例去访问。如果类 A 包含内部类 B,类 B 中包含内部类 C,则在类 A 中不能直接访问类 C,而应该通过类 B 的实例去访问类 C。
- 外部类实例与内部类实例是一对多的关系,也就是说一个内部类实例只对应一个外部类实例,而一个外部类实例则可以对应多个内部类实例。
- 如果实例内部类 B 与外部类 A 包含有同名的成员 t,则在类 B 中 t 和 this.t 都表示 B 中的成员 t,格式:
this.属性名
。而 A.this.t 表示 A 中的成员 t,格式:类名.this.属性名
。- 在实例内部类中不能定义 static 成员,除非同时使用 final 和 static 修饰。
class OuterDemo3 { // 外部类
int a = 1;
public class InnerDemo2 { // 内部类
int a = 2;
int b1 = a;
int c1 = this.a;
int d1 = OuterDemo3.this.a;
}
public static void main(String[] args) {
InnerDemo2 in = new OuterDemo3().new InnerDemo2();
System.out.println(in.b1); // 2
System.out.println(in.c1); // 2
System.out.println(in.d1); // 1
}
}
静态内部类(使用static修饰)
静态内部类是指使用 static 修饰的内部类。
静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问。
静态内部类中可以定义静态成员和实例成员。外部类以外的其他类需要通过完整的类名访问静态内部类中的静态成员,如果要访问静态内部类中的实例成员,则需要通过静态内部类的实例。
在不同位置创建静态内部类对象的区别
在创建静态内部类的实例时,不需要创建外部类的实例。
在外部类以及内部类中,创建静态内部类实例的语法格式:
内部类 变量名 = new 内部类()
。在外部类以外的其他类中,创建内部类实例的语法格式:
外部类.内部类 变量名 = new 外部类.内部类()
。
class OuterDemo4 { // 外部类
static class InnerDemo4 {} // 静态内部类
public static void main(String[] args) {
InnerDemo4 in = new InnerDemo4(); // 创建内部类实例
}
}
class OtherDemo4 {
OuterDemo4.InnerDemo4 in = new OuterDemo4.InnerDemo4(); // 创建内部类实例
}
在静态内部类中访问外部类的成员
静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问。
class OuterDemo6 { // 外部类
int a = 1; // 实例变量
static int b = 2; // 静态变量
static class InnerDemo5 { // 静态内部类
OuterDemo6 out = new OuterDemo6();
int a1 = out.a; // 访问实例变量
static int b1 = b; // 访问静态变量
}
}
在其他类中访问静态内部类成员
静态内部类中可以定义静态成员和实例成员。外部类以外的其他类需要通过完整的类名访问静态内部类中的静态成员,其语法格式:
外部类.内部类.静态成员
。如果要访问静态内部类中的实例成员,则需要通过静态内部类的实例,其语法格式:内部类对象名.实例成员
。其基本上就是创建对象,然后访问对象属性。
class OuterDemo5 { // 外部类
static class InnerDemo5 { // 静态内部类
int a1 = 1; // 实例变量
static int b1 = 2; // 静态变量
}
}
class OtherDemo5 {
OuterDemo5.InnerDemo5 in = new OuterDemo5.InnerDemo5(); // 创建内部类实例
int a = in.a1; // 访问实例变量
int b = OuterDemo5.InnerDemo5.b1; // 访问静态变量
}
局部内部类(定义在方法中)
局部内部类是指在一个方法中定义的内部类。局部内部类有如下特点:
局部内部类与局部变量一样,不能使用访问控制修饰符(public、private 和 protected)和 static 修饰符修饰。默认、常量、抽象、……
局部内部类只在当前方法中有效。
class OuterDemo7 { // 外部类
// 无法在方法外部创建局部内部类实例,找不到该类。
public void action(){
class InnerDemo1{ } // 不加修饰符
final class InnerDemo2{ } // 使用final、抽象、…
InnerDemo1 in = new InnerDemo1(); // 局部内部类只在当前方法中有效
}
}
在局部内部类中访问外部类的成员
局部内部类中不能定义 static 成员。
局部内部类中还可以包含内部类,但是这些内部类也不能使用访问控制修饰符(public、private 和 protected)和 static 修饰符修饰。
在局部内部类中可以访问外部类的所有成员。
在局部内部类中只可以访问当前方法中 final 类型的参数与变量。如果方法中的成员与外部类中的成员同名,则可以使用外部类.this.属性名
的形式访问外部类中的成员。
class OuterDemo8 { // 外部类
int a = 1;
int b = 2;
public void action() {
final int b = 3;
final int c = 4;
class InnerDemo1 {
int a1 = a; // 访问外部类中的成员
int b1 = OuterDemo8.this.b; // 访问外部类中的成员
int b2 = b; // 访问方法中的成员(方法中的成员与外部类中的成员同名时)
int c1 = c; // 访问方法中的成员
}
InnerDemo1 in = new InnerDemo1(); // 局部内部类实例
System.out.println(in.a1);
System.out.println(in.b1);
System.out.println(in.b2);
System.out.println(in.c1);
}
public static void main(String[] args) {
OuterDemo8 out = new OuterDemo8();
out.action();
}
}
Effectively final相关知识补充
Java 中局部内部类和匿名内部类访问的局部变量必须由 final 修饰,以保证内部类和外部类的数据一致性。但从 Java 8 开始,我们可以不加 final 修饰符,由系统默认添加,当然这在 Java 8 以前的版本是不允许的。Java 将这个功能称为 Effectively final 功能。
一个非 final 的局部变量或方法参数,其值在初始化后就从未更改,那么该变量就是 effectively final。
在 Lambda 表达式中,使用局部变量的时候,也要求该变量必须是 final 的,所以 effectively final 在 Lambda 表达式上下文中非常有用。Lambda 表达式在编程中是经常使用的,而匿名内部类是很少使用的。那么,我们在 Lambda 编程中每一个被使用到的局部变量都去显示定义成 final 吗?显然这不是一个好方法。所以,Java 8 引入了 effectively final 新概念。
总结一下,规则没有改变,Lambda 表达式和匿名内部类访问的局部变量必须是 final 的,只是不需要程序员显式的声明变量为 final 的,从而节省时间。
class OuterDemo9 { // 外部类
public void action() {
int a = 1; a = 3; // 变量a初始化后,值发生改变
int b = 2; // 变量b初始化后,值没有改变。默认添加final修饰符
class InnerDemo1 {
// int c = a; // 编译报错
int d = b;
}
}
}
匿名内部类(定义在方法中或者成员上)
匿名内部类是为了减少代码量,如果一个类、抽象类、接口,只需要在运行的时候重写一次,就没必要再去搞个继承什么的,直接new的时候重写一下,然后就可以用了,本质上在底层编译类型是new的那个,运行类型是 匿名内部类;
匿名类是指没有类名的内部类,必须在创建时使用 new 语句来声明类。这种形式的 new 语句声明一个新的匿名类,它对一个给定的类进行扩展,或者实现一个给定的接口。使用匿名类可使代码更加简洁、紧凑,模块化程度更高。
匿名类有两种实现方式:成为子类:继承一个类,重写其方法。成为实现类:实现一个接口(可以是多个),实现其方法。
class OuterDemo10 {
void action() {
System.out.println("我是父类");
}
}
interface OuterDemo11 {
void action();
}
class OtherDemo10 {
// 在方法中构造内部类实例,并调用实例方法
void action() {
// 获取类实例
OuterDemo10 outer = new OuterDemo10() {
// 重写父类中的方法
@Override
void action() {
System.out.println("我是匿名内部类");
}
};
outer.action();
}
void action2(){
OuterDemo11 outer = new OuterDemo11() {
// 重写接口中的方法
@Override
public void action() {
System.out.println("我是匿名内部类2号");
}
};
outer.action();
}
public static void main(String[] args) {
OtherDemo10 other = new OtherDemo10();
other.action();
other.action2();
}
}
在匿名内部类中访问外部类的成员
匿名类和局部内部类一样,可以访问外部类的所有成员。如果匿名类位于一个方法中,则匿名类只能访问方法中 final 类型的局部变量和参数。java8新特性Effectively final:局部变量只赋值一次的默认添加final修饰
匿名类中允许使用非静态代码块进行成员初始化操作。匿名类的非静态代码块会在父类的构造方法之后被执行
class OtherDemo12 {
public static void main(String[] args) {
int a = 1;
final int b = 2; // final
OuterDemo12 outer = new OuterDemo12() {
int c;
{ c = 3; } // 非静态代码块进行成员初始化
@Override
void action() { // 匿名方法中只能访问final类型的参数和局部变量
System.out.println("a的值:" + a + ",b的值:" + b + ",c的值:" + c);
}
};
outer.action();
}
}
内部类实现回调机制
在Java中,可以使用内部类来实现回调机制。回调机制是一种常用的编程模式,用于在一个对象发生某个事件时,通知其他对象执行相应的操作。通过使用内部类作为回调对象,可以方便地实现回调机制,使不同对象之间的耦合度降低,增加代码的灵活性和可扩展性。
使用方式:设置回调对象,执行方法。执行方法的同时,方法内部会调用回调对象中的方法。
interface Callback { // 接口
void onEvent();
}
class EventSource {
private Callback callback; // 设置回调对象
public void setCallback(Callback callback) {
this.callback = callback;
}
public void doSomething() {
System.out.println("执行方法");
// 调用回调方法
if (callback != null) {
callback.onEvent();
}
}
}
class Main {
public static void main(String[] args) {
EventSource event = new EventSource();
event.setCallback(() -> System.out.println("执行回调方法")); // 将内部类设置为回调对象
event.doSomething(); // 执行操作,触发事件
}
}
内部类实现多重继承
Java不支持多重继承,只能单继承,但通过内部类的实现方式,可以模拟实现多重继承的效果。具体实现方法有以下两种:实现不同的接口、继承不同的父类
Java继承存在的原由是为了实现代码的重用和抽象。继承是面向对象编程中的一种重要概念,它允许一个类继承另一个类的属性和方法。以下是几个继承存在的原因:
代码重用:继承允许子类从父类继承属性和方法,子类可以直接使用父类的代码,而不需要重新编写相同的代码。这样可以减少重复的代码,提高代码的复用性。
抽象和泛化:通过继承,可以将一组具有相似属性和方法的类抽象成一个父类,子类可以继承这个父类并添加自己特定的属性和方法。这种抽象和泛化的机制使得代码更加简洁和可维护。
多态性:继承是实现多态性的基础。多态性是指同一个方法可以在不同的对象上产生不同的行为。通过继承,可以将不同的子类对象赋值给父类引用,然后根据实际的子类对象调用相应的方法,实现不同对象的多态行为。
继承关系的建立:继承也用于建立类之间的关系,可以形成类的层次结构,从而更好地组织和管理代码。通过继承,可以将通用的属性和方法定义在父类中,而将具体的实现和特殊的属性放在子类中,使得代码更加清晰和可扩展。继承是Java面向对象编程的核心概念之一,它提供了一种灵活而强大的方式来组织和重用代码,并实现代码的抽象和多态性。
Lambda 表达式
Lambda 表达式是一个匿名函数,基于数学中的λ演算得名,也可称为闭包(Closure)。Lambda 表达式是推动 Java 8 发布的重要新特性,它允许把函数作为一个方法的参数(函数作为参数传递进方法中)
Java 8 采用 Lambda 表达式可以替代匿名内部类。省略new的过程,只保留方法参数和方法体
Lambda 表达式标准语法形式如下:
(参数列表) -> { // Lambda表达式体 };
->被称为箭头操作符或 Lambda 操作符,箭头操作符将 Lambda 表达式拆分成两部分:
左侧:Lambda 表达式的参数列表。
右侧:Lambda 表达式中所需执行的功能,用{ }包起来,即 Lambda 体。
interface Calc { // 可计算接口
int calc(int a, int b, char c); // 计算两个int数值
}
class OtherDemo13 {
public static Calc action() {
// 匿名内部类实现Calc接口
return new Calc() {
@Override
public int calc(int a, int b, char c) {
switch (c) {
case '+': return a + b;
case '-': return a - b;
default: return -1;
}
}
};
}
}
class OtherDemo14 {
public static Calc action() {
// Lambda表达式实现Calc接口
return (int a, int b, char c) -> {
switch (c) {
case '+': return a + b;
case '-': return a - b;
default: return -1;
}
};
}
}
class OtherDemo15 {
public static void main(String[] args) {
Calc action = OtherDemo13.action();
int calc = action.calc(1, 2, '+');
System.out.println(calc);
}
}
函数式接口
Lambda 表达式实现的接口不是普通的接口,而是函数式接口。如果一个接口中,有且只有一个抽象的方法(Object 类中的方法不包括在内),那这个接口就可以被看做是函数式接口。这种接口只能有一个方法。如果接口中声明多个抽象方法,那么 Lambda 表达式会发生编译错误:The target type of this expression must be a functional interface
这说明该接口不是函数式接口,为了防止在函数式接口中声明多个抽象方法,Java 8 提供了一个声明函数式接口注解 @FunctionalInterface。在接口之前使用 @FunctionalInterface 注解修饰,那么试图增加一个抽象方法时会发生编译错误。但可以添加默认方法和静态方法。@FunctionalInterface 注解与 @Override 注解的作用类似,不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
Lambda表达式的三种简写方式
参数类型推断:当Lambda表达式的参数类型可以通过上下文推断出来时,可以省略参数类型的显式声明。
(a, b) -> a + b
单个参数省略括号:当Lambda表达式只有一个参数时,可以省略参数的括号。
a -> a * a
省略花括号和return关键字:当Lambda表达式的主体只有一行代码时,可以省略花括号和return关键字。
(a, b) -> a > b ? a : b
interface Calc2 { // 可计算接口
int calc(int a, int b); // 计算两个int数值
}
interface Calc3 {
int calc(int a);
}
class OtherDemo16 {
public static void main(String[] args) {
int a = 1;
int b = 2;
// 打印加法计算结果。使用匿名内部类
action(new Calc2() {
@Override
public int calc(int a, int b) {
return a + b;
}
}, a, b);
// 打印加法计算结果。使用Lambda表达式替代内部类
action((int x, int y) -> {
return x + y;
}, a, b);
// 打印加法计算结果。使用Lambda表达式简写方式:省略参数类型、省略花括号和return关键字
action((x, y) -> x + y, a, b);
//
action(Integer::sum, a, b);
// 打印平方计算结果。使用匿名内部类
action2(new Calc3() {
@Override
public int calc(int a) {
return a * a;
}
}, b);
// 打印平方计算结果。使用Lambda表达式替代内部类
action2((int x) -> {
return x * x;
}, b);
// 打印平方计算结果。使用Lambda表达式替代内部类:省略参数类型、省略单个参数括号、省略花括号和return关键字
action2(x -> x * x, b);
}
public static void action(Calc2 calc, int a, int b) {
System.out.println(calc.calc(a, b));
}
public static void action2(Calc3 calc, int a) {
System.out.println(calc.calc(a));
}
}
Lambda表达式的使用
作为参数使用Lambda表达式、访问变量、方法引用
作为参数使用
interface Calc2 { // 可计算接口
int calc(int a, int b); // 计算两个int数值
}
class OtherDemo16 {
public static void main(String[] args) {
int a = 1;
int b = 2;
// 打印加法结果
action(new Calc2() {
@Override
public int calc(int a, int b) {
return a + b;
}
}, a, b);
// 打印减法结果
action((int x, int y) -> {
return x - y;
}, a, b);
// 打印乘法结果
action((x, y) -> x * y, a, b);
}
public static void action(Calc2 calc, int a, int b) {
System.out.println(calc.calc(a, b));
}
}
访问变量
Lambda 表达式只能访问局部变量而不能修改,否则会发生编译错误,但对静态变量和成员变量可读可写。
静态方法只能访问静态成员、实例方法可以访问成员(实例、静态)
interface Calc4 {
int calc(int a, int b);
}
class LambdaDemo1 {
private int a = 0;
static int b = 1;
// 静态方法
static Calc4 action() {
return (int x, int y) -> {
b++; // 静态方法只能访问静态成员变量
// a++; // 不能访问实例成员变量
return x + y + b;
};
}
// 实例方法
Calc4 action2() {
int c = 3; // 局部变量必须是final类型的
return (x, y) -> {
a++; // 实例方法可以访问成员变量和成员方法(静态、实例)
b++;
return x + y + a + b + c;
};
}
}
方法引用
方法引用可以理解为 Lambda 表达式的快捷写法,它比 Lambda 表达式更加的简洁,可读性更高,有很好的重用性。如果实现比较简单,复用的地方又不多,推荐使用 Lambda 表达式,否则应该使用方法引用。
Java 8 之后增加了双冒号
::
运算符,该运算符用于“方法引用”,注意不是调用方法,而是将引用传递给真正的方法,调用的是接口中的方法。“方法引用”虽然没有直接使用 Lambda 表达式,但也与 Lambda 表达式有关,与函数式接口有关。 方法引用的语法格式如下:类名::方法名
或者实例名::方法名
注意:被引用方法的参数列表和返回值类型,必须与函数式接口方法参数列表和方法返回值类型一致
当接口作为参数时,其可以接受四种对象:接口实现对象、匿名内部类、Lambda 表达式、方法引用。其中接口实现对象可以使用匿名内部类来代替。如果该接口是函数式接口(有且只有一个抽象的方法),则可以使用Lambda表达式来代替。如果存在一个类的参数列表和返回值类型与函数式接口方法参数列表和方法返回值类型一致,则可以使用方法引用。
注意:方法引用并不是真正的调用方法,只是将引用传递给该接口内部的方法,真正调用的是接口方法。
interface Calc5 {
int calc(int a, int b);
}
class LambdaDemo2 {
/**
* 静态方法,进行加法运算
* 与 int calc(int a, int b)相似,两者的参数列表与返回值都相同
*/
public static int add(int a, int b) {
return a + b;
}
/**
* 实例方法,进行减法运算
* 与 int calc(int a, int b)相似,两者的参数列表与返回值都相同
*/
public int sub(int a, int b) {
return a - b;
}
public static void display(Calc5 calc, int a, int b) {
System.out.println(calc.calc(a, b)); // 打印计算结果
}
public static void main(String[] args) {
int a = 1;
int b = 2;
display(LambdaDemo2::add, 1, 2); // 打印加法计算结果,int add(int a, int b)
display(Integer::sum, a, b); // Integer的sum()方法,int sum(int a, int b)
LambdaDemo2 d = new LambdaDemo2();
display(d::sub, a, b); // LambdaDemo2的sub()方法,int sub(int a, int b)
}
}
对象(创建、使用、三大特性)
使用new关键词创建对象,使用
对象.方法
执行类中定义好的方法,封装继承多态是面向对象的三大特性。封装的目的就是为了代码重用,典型的封装就是类中的方法。
继承的目的是为了抽象和代码重用,子类会继承父类的属性和方法,抽象出父类。
多态的基础是继承,多态性是指同一个方法可以在不同的对象上产生不同的行为,可以产生多种效果。通过继承,可以将不同的子类对象赋值给父类引用,然后根据实际的子类对象调用相应的方法,实现不同对象的多态行为。
class MyDemo {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void action() {
System.out.println("父类");
}
}
class Son extends MyDemo {
public void action() {
System.out.println("子类");
}
}
class MyTest {
public static void main(String[] args) {
MyDemo myDemo = new MyDemo();
myDemo.setName("我是父类实例,执行父类方法");
String name = myDemo.getName();
myDemo.action();
Son son = new Son();
son.setName("我是子类实例,执行子类方法");
String name1 = son.getName();
son.action();
MyDemo myDemo1 = new Son();
myDemo1.setName("子类实例赋值父类实例,这是多态,实际执行子类方法");
String name2 = myDemo1.getName();
myDemo1.action();
}
}
基本数据类型
整型、浮点型、字符型、布尔型
byte、short、int、long、float、double、char、boolean
基本数据类型相关知识
基本数据类型是用于表示数据的原始类型,它们是预定义的,并且不是对象。
分类:Java中共有8种基本数据类型:byte、short、int、long、float、double、char、boolean
特点:
基本数据类型具有固定的大小和范围,不需要进行实例化。
基本数据类型在内存中存储的是实际的数据值,而不是对象的引用。
基本数据类型的赋值是通过复制数据的值完成的,而不是引用的传递。
用途:可以使用基本数据类型来定义变量,存储和操作数据
/**整型:byte、short、int、long*/
byte b = 10; // 8位有符号整数类型,占一个字节,-2^7~2^7-1 -128~127 有两个零
short s = 1000; // 16位有符号整数类型,占两个字节 -2^15~2^15-1 -32768~32767 有四个零
int i = 100000; // 32位有符号整数类型,占四个字节 -2^31~2^31-1 -21亿~21亿(大约) 有九个零
long l = 1000000L; // 64位有符号整数类型,L表示长整型,占八个字节 -2^63~2^63-1 很大
/**浮点型:float、double*/
float f = 3.14f; // 32位单精度浮点数类型,f表示浮点数
double d = 3.14; // 64位双精度浮点数类型
/**
* 字符型:char
* Java的字符是unicode数字,是一个16位无符号的整数,占两个字节,0~65535
*/
char c = 'A'; // 16位Unicode字符类型,其数字为65
/**布尔型:boolean*/
boolean flag = true; // 布尔类型,取值为true或false
/**
注意点:
Java在数学计算的时候不检查范围,在超范围计算的时候会发生溢出现象,溢出计算是有害的!需要在编程中了解极值加以避免。
为了追求计算的准确性,常使用double类型,很少使用float类型
小数常量默认的类型是double类型,例如:5.5
字面量的后缀:L/l D/d F/f
字面量:直接给出的常数,也称为"直接量"
浮点数计算不保证绝对的准确性
*/
引用数据类型
字符串、数字、时间、数组、集合、IO、线程、
字符串(String、Stringbuffer、Stringbuilder)
String:不可变字符串类,被声明为 final class,是不可变字符串。因为它的不可变性,所以拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。可以使用其他变量重新赋值的方式进行更改。
StringBuffer:可变字符串类,可以随意修改字符串的内容,该对象的容量会自动扩大(如果字符串长度超出现有容量)。
StringBuilder:可变字符串类,可以随意修改字符串的内容,该对象的容量会自动扩大(如果字符串长度超出现有容量)。
长度是否可变:String不可变、StringBuilder可变、StringBuffer可变
线程是否安全:String线程安全、StringBuffer线程安全、StringBuilder线程不安全
实现接口是否相同:String类直接实现CharSequence接口、 StringBuffer、StringBuilder继承于AbstractStringBuilder,实现CharSequence接口。CharSequence是一个定义字符串操作的接口,它只包括length()、charAt(int index)、subSequence(int start, int end) 这几个 API。速度:一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的。
使用环境:操作少量的数据使用 String。 单线程操作大量数据使用 StringBuilder。多线程操作大量数据使用 StringBuffer。
String相关知识
在Java中,字符串(String)是一种表示文本数据的数据类型。它是由一系列Unicode字符组成的序列,用于存储和操作文本信息,如字母、数字、符号等。
java字符串拥有下列功能:
存储文本数据:字符串可以用于存储用户输入、文件内容、网络通信等各种文本数据。
文本处理:字符串允许执行各种文本处理操作,如查找子字符串、替换文本、分割字符串等。
格式化:通过字符串格式化,可以将不同类型的数据转换为文本形式,用于输出和显示。
注意事项:
1、字符串是不可变的:被声明为 final class,是不可变字符串。一旦创建,字符串内容就不能被改变。对字符串的任何修改都会创建新的字符串对象。频繁修改字符串会产生很多无用的中间对象,进而影响性能。
2、字符串连接:频繁的字符串连接操作可能会导致性能问题,因为每次连接都会创建一个新的字符串。可以考虑使用 StringBuilder 或 StringBuffer 来优化连接操作。
3、字符编码:在处理字符串时,要注意字符编码,特别是涉及到不同字符集之间的转换和处理。
4、== 操作符和 equals() 方法:使用 equals() 方法来比较字符串内容是否相等,而不是使用 == 操作符,因为 == 操作符比较的是引用地址。
赋值的两种方式
/**
* 赋值String对象的几种方式:使用字面量、使用构造器
*/
String str1; str1 = "字符串1"; // 先声明,再赋值
String str2 = "字符串2"; //声明后直接赋值
String str3 = new String(); //空字符
String str4 = new String("字符串4"); // 使用构造器
String str5 = new String(str4); // 创建该字符串的副本,即它们的值是相等的,使用同一个内存空间
String str6 = new String(new char[]{'a','b','c','d'}); // abcd
String str7 = new String(new char[]{'a','b','c','d'},2,1); // c
String数据和基本数据之间的转换
XXX.parseXxx(字符串):将字符串转成基本类型
String.valueOf(基本类型):将基本类型转成字符串
使用运算符:将基本类型转成字符串
字符串方法:从字符串中获取字符、字符串等数据
/**
* 将字符串转成基本数据类型:
* 使用 包装类.parseXxx(字符串)
*/
int i = Integer.parseInt("123");
float f = Float.parseFloat("123.456");
boolean b = Boolean.parseBoolean("true");
/**
* 将基本数据类型转成String字符串:
* 使用 String.valueOf(基本类型)
*/
String s1 = String.valueOf(123);
String s2 = String.valueOf(123.456);
String s3 = String.valueOf(true);
String s4 = String.valueOf('A');
/**
* 将基本数据类型转成String字符串:
* 使用“+”运算符连接字符串时,会将基本数据类型自动转换成String类型
*/
String s5 = "" + 123;
String s6 = "" + 123.456;
String s7 = "" + true;
String s8 = "" + 'A';
/**
* 从String字符串中获取字符:
* 指定下标的字符、转成字符数组获取字符
*/
String str ="CSDN";
char c1 = str.charAt(0);
char c2 = str.toCharArray()[0];
String字符串方法:
长度
length()
、获取子串substring()
、分割split()
、比较equals()、equalsIgnoreCase()
、查找indexOf()、lastIndexOf()、charAt()
、替换replace()、replaceAll()、replaceFirst()
、去除首尾空格trim()
、…
使用展示:
/**
* 字符串方法示例:
*/
public class MyDemo {
public static void main(String[] args) {
String str1 = "123456789";
String str2 = "红,绿,蓝,白,黑";
String str3 = "AB_AB_AB_AB";
/**
* 长度:
* length():获取字符串的长度。
*/
int length = str1.length(); // 9
/**
* 获取子串
* substring(int beginIndex):获取从指定的索引位置开始到字符串末尾的子字符串。
* substring(int beginIndex, int endIndex):获取字符串的一部分子字符串(前包后不包)。
* 注意事项:获取操作对原先的字符串不做任何的改变
*/
String result1 = str1.substring(3); // 456789
String result2 = str1.substring(3, 6); // 456
/**
* 分割
* split(String str):根据指定的分割符号将字符串分割成字符串数组。
* split(String str,int num):指定的分割符分割,指定分割后生成的数组元素个数
* 注意事项:“.”和“|”都是转义字符,必须得加“\\”。
*/
String[] result3 = str2.split(","); // ["红","绿","蓝","白","黑"]
String[] result4 = str2.split("," ,3); // ["红","绿","蓝,白,黑"]
/**
* 比较
* equals(String str):比较字符串内容是否相等。
* equalsIgnoreCase(String str):不区分大小写比较字符串内容、长度是否相同。
* compareTo():用于按字典顺序比较两个字符串的大小,该比较是基于字符串各个字符的 Unicode 值。
*/
String str4 = "Hello";
String str5 = "hello";
String str6 = str4;
boolean b1 = str4.equals("Hello"); // true
boolean b2 = str4.equals(str5); // false
boolean b3 = str4.equals(str6); // true
boolean b4 = "abc".equalsIgnoreCase("ABC"); // true
int num = "AC".compareTo("Abd"); // -31
// 比较第一个字符,如果第一个字符相同,比较第二个字符,A对应65,a对应97,两者相差32
/**
* 查找字符串
* 返回值:如果存在该字符串,返回该字符串索引,不存在,返回-1
* indexOf(String str):正序查找指定字符串
* indexOf(String value,int fromIndex); 指定查找时的起始索引,正序查找字符串
* lastIndexOf(String value); 倒序查找指定字符串
* lastIndexOf(String value,int fromIndex); 指定查找时的起始索引,倒序查找字符串
*/
int index1 = str3.indexOf("_"); // 2
int index2 = str3.indexOf("_",6); // 8
int index3 = str3.lastIndexOf("_"); // 8
int index4 = str3.lastIndexOf("_",6); // 5
/**
* 查找索引对应的字符
* charAt(int index):获取指定位置的字符。
*/
char c = str1.charAt(0); // 返回'1'
/**
* 替换
* replace(String old, String new):替换字符串中的指定字符序列。
* replaceAll(String old, String new):替换字符串中的指定字符序列。
* replaceFirst(String old, String new):正序查找,替换首个指定字符序列
* 用途:替换错别字、和谐用语
*/
String newStr1 = str3.replace("_", ""); // ABABABAB
String newStr2 = str3.replaceAll("_", ""); // ABABABAB
String newStr3 = str3.replaceFirst("_", ""); // ABAB_AB_AB
/**
* 去除首尾空格
* trim():去除字符串中的首尾空格
*/
String str7 = " Hello World ";
String newStr4 = str7.trim(); // 去除首尾空格,返回"Hello World"
String newStr5 = str7.replace(" ",""); // 去除所有空格,返回"HelloWorld"
/**
* 转换大小写
* toLowerCase():将大写字母转换为小写字母
* toUpperCase():将小写字母转换为大写字母
*/
String s1 = "ABC.123abc".toLowerCase(); // abc.123.abc
String s2 = "ABC.123abc".toUpperCase(); // ABC.123ABC
}
}
Stringbuffer相关知识
StringBuffer:可变字符串类,可以随意修改字符串的内容,该对象的容量会自动扩大(如果字符串长度超出现有容量)。
创建StringBuffer对象:
构造器:
new StringBuffer()
/**
* 赋值StringBuffer对象的几种方式:
*
* 无参:new StringBuffer()
* 有参:new StringBuffer(int length)
* 有参:new StringBuffer(String str)
* 链式:new StringBuffer().append(String str)
*/
StringBuffer sb1 = new StringBuffer(); // 无参构造器,空的字符串缓冲区
StringBuffer sb2 = new StringBuffer(16); // 初始容量16的空字符串缓冲区
StringBuffer sb3 = new StringBuffer("荒");// 初始化内容为“荒”的字符串缓冲区
StringBuffer sb4 = new StringBuffer().append("荒").append("血色平原");// 使用append()方法链式调用
StringBuffer字符串缓冲区方法:
追加
append()
、插入insert()
、替换replace()、setCharAt()
、反转reverse()
、删除delete()、deleteCharAt()
、转StringtoString()
、容量capacity()
/**
* StringBuffer方法示例:
*/
class MyDemo {
public static void main(String[] args) {
StringBuffer sb1 = new StringBuffer();
StringBuffer sb2 = new StringBuffer("云曦");
StringBuffer sb3 = new StringBuffer("12345,上山打老虎");
StringBuffer sb4 = new StringBuffer("12345,上山打老虎");
StringBuffer sb5 = new StringBuffer("12345,上山打老虎");
StringBuffer sb6 = new StringBuffer("12345,上山打老虎");
StringBuffer sb7 = new StringBuffer("12345,上山打老虎");
StringBuffer sb8 = new StringBuffer("12345,上山打老虎");
/**
* 追加
* append():用于将指定的字符串、字符或其他类型的数据追加到当前StringBuffer对象的末尾。
*/
sb1.append(12345); // 在末尾追加整数12345
sb1.append(','); // 在末尾追加字符','
sb1.append('上山打老虎'); // 在末尾追加字符串"上山打老虎" // 12345,上山打老虎
/**
* 插入
* insert():用于在指定的位置插入字符串、字符或其他类型的数据到当前StringBuffer对象中。
*/
sb2.insert(2, "灵犀耳坠"); // 在索引位置2插入字符串"灵犀耳坠"
sb2.insert(2, ","); // 在索引位置2插入字符',' // 云曦,灵犀耳坠
/**
* 替换
* replace():用于替换指定范围内的字符或字符串。
* setCharAt():在字符串的指定索引位置替换一个字符。
*/
sb3.replace(6, 11, "武松去买酒"); // 替换索引位置6到10的字符 // 12345,武松去买酒
sb4.setCharAt(2, '哈'); // 12哈45,上山打老虎
/**
* 反转
* reverse():将字符串序列用其反转的形式取代。
*/
sb5.reverse(); // 虎老打山上,54321
/**
* 删除
* delete():用于删除指定范围内的字符。
* deleteCharAt():移除序列中指定位置的字符
*/
sb6.delete(2, 5); // 删除索引位置2到4的字符 // 12,上山打老虎
sb7.deleteCharAt(2); // 1245,上山打老虎
/**
* 转String
* toString():用于将当前StringBuffer对象转换为String对象。
*/
String str = sb8.toString(); // 12345,上山打老虎
/**
* 容量
* capacity():获取当前StringBuffer缓冲区对象容量,默认16
*/
int capacity = new StringBuffer().capacity(); // 16
}
}
Stringbuilder相关知识
StringBuilder:可变字符串类,可以随意修改字符串的内容,该对象的容量会自动扩大(如果字符串长度超出现有容量)。
创建StringBuilder对象:
构造器:
new StringBuilder()
/**
* 赋值StringBuilder对象的几种方式:
*/
StringBuilder sb1 = new StringBuilder(); // 使用无参构造函数
StringBuilder sb2 = new StringBuilder(16); // 使用带有初始容量参数的构造函数
StringBuilder sb3 = new StringBuilder("荒");// 使用字符串作为初始值
StringBuilder sb4 = new StringBuilder().append("荒").append("血色平原");// 使用append()方法链式调用
StringBuilder字符串缓冲区方法:
追加
append()
、插入insert()
、替换replace()、setCharAt()
、反转reverse()
、删除delete()、deleteCharAt()
、转StringtoString()
、容量capacity()
/**
* 内容同StringBuffer
*/
数字(Random、Math、DecimalFormat、BigInteger、BigDecimal)
Random相关知识
Random 类提供了丰富的随机数生成方法,可以生成不同类型的随机数,如整数、浮点数、布尔值
Random方法:
返回随机整数
nextlnt()、nextLong()
、返回随机小数nextFloat()、nextDouble()
、返回随机布尔值nextBoolean()
、``
/**
* Random方式示例:
*/
class MyDemo {
public static void main(String[] args) {
Random random = new Random();
/**
* 随机整数
* nextInt():返回一个随机的int值(-2^31~2^31-1 也就是-21亿~21亿)
* nextInt(int n):n>0,生成区间[0,n)的随机整数,可以根据计算的方式得出所需取值区间
* nextLong():返回一个随机长整型数字
*/
int i1 = random.nextInt(); // 随机int值,-21亿~21亿
int i2 = random.nextInt(10); // 生成区间[0,10)的随机整数,
long l = random.nextLong();
/**
* 随机小数
* nextFloat():返回一个区间[0,1.0)的单精度小数。可以根据计算的方式得出所需取值区间
* nextDouble():返回一个区间[0,1.0)的双精度小数。可以根据计算的方式得出所需取值区间
*/
float v = random.nextFloat();
double v1 = random.nextDouble();
/**
* 随机布尔值
* nextBoolean():返回一个随机布尔值
*/
boolean b = random.nextBoolean();
/**
* 应用:几率实现。假设我们有三个选项A、B和C,它们的概率分别为0.4、0.3和0.3。
*/
private static String getRandomOption() {
double randomValue = Math.random();
if (randomValue < 0.4) {
return "A";
} else if (randomValue < 0.7) {
return "B";
} else {
return "C";
}
}
}
}
Math相关知识
DecimalFormat相关知识
DecimalFormat 是 NumberFormat 的一个子类,用于格式化十进制数字。
DecimalFormat方法:
DecimalFormat 类包含一个模式和一组符号。模式
format()
,符号0 # . - , E % ?
/**
* 0 显示数字,四舍五入,如果位数不够则补 0
*/
System.out.println(new DecimalFormat("0.0").format(123.456)); // 123.5
System.out.println(new DecimalFormat("00.000000").format(123.456)); //123.456000
/**
* # 显示数字,四舍五入,如果位数不够不发生变化
*/
System.out.println(new DecimalFormat("#.#").format(123.456)); //123.5
System.out.println(new DecimalFormat("##.######").format(123.456)); //123.456
BigInteger相关知识
大数字运算:BigInteger
如果要存储比 Integer 更大的数字,Integer 数据类型就无能为力了。因此,Java 中提供 BigInteger 类来处理更大的数字。BigInteger 类型的数字范围较 Integer 类型的数字范围要大得多。BigInteger 支持任意精度的整数,也就是说在运算中 BigInteger 类型可以准确地表示任何大小的整数值。除了基本的加、减、乘、除操作之外,BigInteger 类还封装了很多操作,像求绝对值、相反数、最大公约数以及判断是否为质数等。
创建BigInteger对象:
语法格式:
BigInteger(String val)
这里的 val 是数字十进制的字符串。
BigInteger bi = new BigInteger("9");
BigInteger方法展示:
BigInteger 类的常用运算方法:
add(BigInteger val) 做加法运算
subtract(BigInteger val) 做减法运算
multiply(BigInteger val) 做乘法运算
divide(BigInteger val) 做除法运算
remainder(BigInteger val) 做取余数运算
divideAndRemainder(BigInteger val) 做除法运算,返回数组的第一个值为商,第二个值为余数
pow(int exponent) 做参数的 exponent 次方运算
BigInteger bi = new BigInteger("9");
System.out.println("加法操作结果:" + bi.add(new BigInteger("99"))); // 108
System.out.println("减法操作结果:" + bi.subtract(new BigInteger("25"))); // -16
System.out.println("乘法橾作结果:" + bi.multiply(new BigInteger("3"))); // 27
System.out.println("除法操作结果:" + bi.divide(new BigInteger("2"))); // 4
System.out.println("取商操作结果:" + bi.divideAndRemainder(new BigInteger("3"))[0]); // 3
System.out.println("取余操作结果:" + bi.divideAndRemainder(new BigInteger("3"))[1]); // 0
System.out.println("取 2 次方操作结果:" + bi.pow(2)); // 81
BigDecimal相关知识
BigInteger 和 BigDecimal 都能实现大数字的运算,不同的是 BigDecimal 加入了小数的概念。一般的 float 和 double 类型数据只能用来做科学计算或工程计算,但由于在商业计算中要求数字精度比较高,所以要用到 BigDecimal 类。BigDecimal 类支持任何精度的浮点数,可以用来精确计算货币值。BigDecimal 类的方法可以用来做超大浮点数的运算,像加、减、乘和除等。在所有运算中,除法运算是最复杂的,因为在除不尽的情况下,末位小数的处理方式是需要考虑的。
创建BigDecimal 对象:
BigDecimal 常用的构造方法如下。
BigDecimal(double val)
:实例化时将双精度型转换为 BigDecimal 类型。
BigDecimal(String val)
:实例化时将字符串形式转换为 BigDecimal 类型。
BigDecimal bd = new BigDecimal( 3.14 + "");
BigDecimal方法展示:
BigDecimal 类用于实现加、减、乘和除运算的方法。
BigDecimal add(BigDecimal augend) 加法操作
BigDecimal subtract(BigDecimal subtrahend) 减法操作
BigDecimal multiply(BigDecimal multiplieand) 乘法操作
BigDecimal divide(BigDecimal divisor,int scale,int roundingMode ) 除法操作
divide() 方法的 3 个参数分别表示除数、商的小数点后的位数和近似值处理模式。
BigDecimal bd = new BigDecimal( 3.14 + "");
System.out.println("加法操作结果:" + bd.add(new BigDecimal(99.154)));
System.out.println("减法操作结果:" + bd.subtract(new BigDecimal(-25.157904)));
System.out.println("乘法操作结果:" + bd.multiply(new BigDecimal(3.5)));
System.out.println("除法操作结果(保留 2 位小数):" + bd.divide(new BigDecimal(3.14), 2, BigDecimal.ROUND_CEILING));
roundingMode参数补充说明:
roundingMode参数支持的处理模式:
BigDecimal.ROUND_UP:商的最后一位如果大于 0,则向前进位,正负数都如此
BigDecimal.ROUND_DOWN:商的最后一位无论是什么数字都省略
BigDecimal.ROUND_CEILING:商如果是正数,按照 ROUND_UP 模式处理;如果是负数,按照 ROUND_DOWN模式处理
BigDecimal.ROUND_FLOOR:与 ROUND_CELING 模式相反,商如果是正数,按照 ROUND_DOWN 模式处理;如果是负数,按照 ROUND_UP 模式处理
BigDecimal.ROUND_HALF_ DOWN:对商进行五舍六入操作。如果商最后一位小于等于 5,则做舍弃操作,否则对最后一位进行进位操作
BigDecimal.ROUND_HALF_UP:对商进行四舍五入操作。如果商最后一位小于 5,则做舍弃操作,否则对最后一位进行进位操作
BigDecimal.ROUND_HALF_EVEN:如果商的倒数第二位是奇数,则按照 ROUND_HALF_UP 处理;如果是偶数,则按照 ROUND_HALF_DOWN 处理
简单理解:
BigDecimal.ROUND_UP:在精度最后一位加一个单位:1.666 =>1.67 1.011 =>1.02 1.010 =>1.01 该模式永远不会减少被操作的数的值
BigDecimal.ROUND_DOWN:截端操作,类似truncate 该模式永远不会增加被操作的数的值
时间(Date、Calendar、String、long、SimpleDateFormat、java.time包)
Date类:该类的每一个实例用来描述时间,内部维护了一个long值,记录自1970年元旦到这一刻之间的毫秒差
Calendar类:日历类,用于操作时间的类,Calendar是一个抽象类,不能直接实例化,使用提供的工厂方法获取实例:
getInstance()
String类:该类可以描述一段字符串形式的时间。例如:“2023-03-31 16:18:09”、“2023年08月24日16时22分16秒”
long类:该类可以描述毫秒值。例如:20000000,其中 1000 毫秒等于 1 秒。
java.time包:Java 8中,引入了新的日期和时间API,即java.time包
Date相关知识
创建Date对象
构造器:
new Date()
、将给定的字符串解析成日期:sdf.parse()
Date date1 = new Date();
Date date2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2023-08-24 16:22:16");
Date对象方法展示:
判断指定日期之后:
after()
、判断指定日期之前:before()
、设置毫秒值时间setTime()
、获取毫秒值时间getTime()
、…,省略的方法中大多都已经过时了
class MyDemo {
public static void main(String[] args) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");
Date date = sdf.parse("2023年08月24日17时05分32秒");
/**
* getTime():获取指定时间的毫秒值(从1970年1月1日开始计时)
*/
long time = date.getTime(); //1692867932000
/**
* setTime():设置指定时间的毫秒值
*/
date.setTime(1692867932000L);
String str = sdf.format(date); // 2023年08月24日17时05分32秒
/**
* after():判断此日期是否在指定日期之后
* before():判断此日期是否在指定日期之前
*/
boolean after = date.after(new Date()); // false
boolean before = date.before(new Date()); // true
System.out.println(time);
System.out.println(str);
System.out.println(after);
System.out.println(before);
}
}
SimpleDateFormat相关知识
SimpleDateFormat类:该类使用一个特殊的字符串来描述一个时间格式。
SimpleDateFormat 是一个以与语言环境有关的方式来格式化和解析日期的具体类,它允许进行格式化(日期→文本)、解析(文本→日期)和规范化。
创建SimpleDateFormat对象:
/**
* 时间格式字符串中的特殊字符:
y:表示年 yyyy:表示4位数的年 yy:表示2位数的年
M:表示月 MM:表示两位数的月
D:年份中的天数。表示当天是当年的第几天
d:表示日 dd:表示两位数的日
H/h:表示小时 HH/hh:表示两位数的小时 HH:表示24小时制 hh:12小时制
m:表示分钟 mm:表示两位数的分钟
s:表示秒 ss:表示两位数的秒
E:表示星期,会根据语言环境的不同,显示不同语言的星期几
a:表示上下午
*/
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");
方法展示:
返回Date对象:
parse()
、返回String对象:format()
class MyDemo8 {
public static void main(String[] args) throws ParseException {
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒");
/**
* format(Date date):返回String对象
*/
String str1 = sdf1.format(new Date()); // 2023-08-24 16:22:16
String str2 = sdf2.format(new Date()); // 2023年08月24日16时22分16秒
/**
* parse(String time):返回Date对象
*/
Date date1 = sdf1.parse(str1);
Date date2 = sdf2.parse(str2);
}
}
Calendar相关知识
Calendar类是用于处理日期和时间的类。它提供了许多方法来操作日期和时间,包括获取、设置、增加和减少日期和时间的各个字段。
创建Calendar对象:
Calendar.getInstance()
Calendar calendar = Calendar.getInstance();
Calendar常量展示:
年:
Calendar.YEAR
月:Calendar.MONTH
日:Calendar.DATE 年中xx天、Calendar.DAY_OF_MONTH 月中xx天
时:Calendar.HOUR 12小时制、Calendar.HOUR_OF_DAY 24小时制
分:Calendar.MINUTE
秒:Calendar.SECOND
星期:Calendar.DAY_OF_WEEK
Calendar对象方法展示:
获取时间日期值:
get()
设置时间日期值:set()
添加减少日期时间值:add()
返回 Calendar 时间值的 Date 对象:getTime()
用给定的 Date 值设置 Calendar 的当前时间值:setTime(Date date)
返回 Calendar 时间值的毫秒时间:getTimeInMillis()
用给定的 long 值设置 Calendar 的当前时间值:setTimeInMillis(long millis)
class MyDemo {
public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
/**
* 获取日期和时间的各个字段的值:
* get(Calendar常量):
*/
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
/**
* 设置日期和时间的各个字段的值:
* set(Calendar常量, int value):
* set(int year, int month,int hourOfDay):设置日期和时间的各个字段的值:
* set(int year, int month, int date, int hourOfDay, int minute,int second);
*/
calendar.set(Calendar.YEAR, 2022);
calendar.set(Calendar.MONTH, Calendar.JANUARY);
calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(2023, Calendar.AUGUST,30);// 月份从数值0开始
calendar.set(2023, Calendar.AUGUST,30,14,34);
/**
* 增加或减少日期和时间的各个字段的值:
* add(Calendar常量, int value):
*/
calendar.add(Calendar.YEAR, 1);
calendar.add(Calendar.MONTH, -1);
calendar.add(Calendar.DAY_OF_MONTH, 7);
calendar.add(Calendar.HOUR_OF_DAY, 12);
calendar.add(Calendar.MINUTE, -30);
calendar.add(Calendar.SECOND, 10);
/**
* 返回一个表示此 Calendar 时间值的 Date 对象
* Date getTime()
*/
Date date = calendar.getTime();
/**
* 返回此 Calendar 的时间值,以毫秒为单位
* long getTimeInMillis()
*/
Date time = calendar.getTime();
/**
* 用给定的 Date 值设置此 Calendar 的当前时间值
* void setTime(Date date)
*/
calendar.setTime(new Date());
/**
* 用给定的 long 值设置此 Calendar 的当前时间值
* void setTimeInMillis(long millis)
*/
calendar.setTimeInMillis(1692867932000L);
}
}
String、Date、Calendar三者转换相关知识
SimpleDateFormat:Date <=> String 两者相互转换的桥梁
Calendar:Date <=> Calendar 两者相互转换的桥梁、long <=> Calendar 互转桥梁
Date:Calendar <=> String 两者相互转换的桥梁、long <=> Date 互转桥梁
/**
* String <=> Date 两者相互转换
*/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date parse = sdf.parse("2023-08-17 10:04:50");
String format = sdf.format(new Date());
/**
* Date <=> Calendar 两者相互转换
*/
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
Date date = calendar.getTime();
/**
* Calendar <=> String 两者相互转换
*/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Calendar calendar = Calendar.getInstance();
String dateString = sdf.format(calendar.getTime());
calendar.setTime(sdf.parse("2023-08-09 12:00:00"));
/**
* long <=> Date 两者相互转换
*/
Date date = new Date(1000000L);
long timestamp = date.getTime();
/**
* long <=> Calendar 两者相互转换
*/
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(100000L);
calendar.getTimeInMillis();
/**
* 注意:long代表时间时,从1970-01-01 00:00:00开始计时,+8小时,因为北京时间+8
*/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date(100000);
System.out.println(sdf.format(date)); // 1970-01-01 08:01:40
java.time包相关知识:
java.time.LocalDate
:表示一个ISO-8601格式的日期,不包含时间信息
java.time.LocalTime
:表示一个ISO-8601格式的时间,不包含日期信息
java.time.LocalDateTime
:表示一个ISO-8601格式的日期时间
java.time.Instant
:表示从‘1970-01-01T00:00:00Z’开始的秒数
java.time.Duration
:表示两个时刻之间的持续时间或时间段
java.time.Period
:表示两个日期之间的天数、月数或年数
java.time.ZonedDateTime
:表示带时区的日期时间信息
java.time.DateTimeFormatter
:用于格式化和解析日期时间信息
…
创建LocalDateTime对象
使用静态方法now()获取当前日期。
使用静态方法of()传入年、月、日、时、分、秒参数创建指定日期。
使用静态方法parse()传入日期字符串和日期格式创建日期对象。
/**
* 创建LocalDateTime对象
*/
LocalDateTime now = LocalDateTime.now();
LocalDateTime of = LocalDateTime.of(2023, 8, 25, 14, 2, 1);
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dt = LocalDateTime.parse("2017-12-03 10:15:30", pattern);
// 严格按照ISO yyyy-MM-dd验证,03写成3都不行
展示LocalDateTime对象方法:
获取、增加、减少、设置、判断、格式化
- 获取指定日期时间的年、月、日、时、分、秒。
getYear()、getMonth()、getDayOfMonth()、getHour()、getMinute()、getSecond()- 增加指定的年、月、日、时、分、秒。
plusYears()、plusMonths()、plusDays()、plusHours()、plusMinutes()、plusSeconds()- 减少指定的年、月、日、时、分、秒。
minusYears()、minusMonths()、minusDays()、minusHours()、minusMinutes()、minusSeconds()- 设置指定日期时间的年、月、日、时、分、秒。
withYear()、withMonth()、withDayOfMonth()、withHour()、withMinute()、withSecond()- 判断当前日期时间是否早于或晚于指定的日期时间。isBefore()、isAfter()
- 将日期时间格式化为指定的字符串。format()
/**
* 展示LocalDateTime对象方法:
* 请注意,LocalDateTime类是不可变的,这意味着每次操作都会返回一个新的实例。
*/
class MyDemo12 {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime of = LocalDateTime.of(2023, 1, 1, 1, 0, 0);
/**
* 获取指定日期时间的年、月、日、时、分、秒。
* 注意:Month month = now.getMonth(); int value = month.getValue();
*/
int year = now.getYear();
int month = now.getMonth().getValue();
int dayOfMonth = now.getDayOfMonth();
int hour = now.getHour();
int minute = now.getMinute();
int second = now.getSecond();
/**
* 增加指定的年、月、日、时、分、秒。
*/
LocalDateTime newDateTime1 = now.plusYears(1);
/**
* 减少指定的年、月、日、时、分、秒。
*/
LocalDateTime newDateTime2 = now.minusMonths(1);
/**
* 设置指定日期时间的年、月、日、时、分、秒。
*/
LocalDateTime newDateTime3 = now.withYear(2022);
/**
* 判断当前日期时间是否早于或晚于指定的日期时间。
* now为当前日期时间,of为指定日期时间
*/
boolean isBefore = now.isBefore(of); // false
boolean isAfter = now.isAfter(of); // true
/**
* 将日期时间格式化为指定的字符串。
*/
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = now.format(pattern); // 2023-08-26 14:52:41
}
}
LocalDate、LocalTime、LocalDateTime三者方法一致的原因
LocalDateTime、LocalDate和LocalTime三者所拥有的方式大致相同是因为它们都实现了TemporalAccessor和Temporal接口。
TemporalAccessor接口是一个时间访问器接口,它定义了访问日期和时间字段值的方法。通过实现这个接口,LocalDateTime、LocalDate和LocalTime可以提供获取年、月、日、时、分、秒等字段值的方法。
Temporal接口是一个时间接口,它定义了对日期和时间进行计算、修改和操作的方法。通过实现这个接口,LocalDateTime、LocalDate和LocalTime可以提供对日期和时间进行加减、比较、格式化等操作的方法。
创建和使用LocalDate、LocalTime对象
LocalDate 表示一个不可变的日期对象,它只包含有关年、月、日信息,不包含任何关于时间的信息。
LocalDate对象的创建方式以及使用方法和LocalDateTime对象大致相同
Date转String对比LocalDateTime转String:
class MyDemo {
public static void main(String[] args) throws ParseException {
dateString();
stringDate();
}
/**
* Date转Sting LocalDateTime转String
*/
public static void dateString() {
SimpleDateFormat sdf = new SimpleDateFormat("G yyyy年MM月dd号 E a hh时mm分ss秒");
Date date = new Date();
System.out.println(sdf.format(date));
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("G yyyy年MM月dd号 E a hh时mm分ss秒");
LocalDateTime now = LocalDateTime.now();
System.out.println(now.format(pattern));
}
/**
* Sting转Date Sting转LocalDateTime
*/
public static void stringDate() throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date date = sdf.parse("2017-12-03 10:15:30");
System.out.println(sdf.format(date));
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dt = LocalDateTime.parse("2017-12-03 10:15:30", pattern);
// 严格按照ISO yyyy-MM-dd验证,03写成3都不行
System.out.println(dt.format(pattern));
}
}
数组(xxx[]、Arrays、比较器)
xxx[]相关知识
在Java中,数组是一种引用类型,是用于存储固定数量元素的数据结构,是一种复合数据类型,是有序数据的集合,数组中的每个元素具有相同的数据类型,是一个储存容器。可以用一个统一的数组名和不同的下标来确定数组中唯一的元素。根据数组的维度,可以将其分为一维数组、二维数组和多维数组等。
声明和初始化数组:
在Java中变量的使用必须要声明和初始化:
声明数组变量:dataType[] arrayName;
,其中的数据类型既可以是基本数据类型,也可以是引用数据类型
分配存储空间:通过使用 new 指定数组大小(数组存储空间大小)arrayName = new dataType[length];
简化上述两者操作:dataType[] arrayName = new dataType[length];
初始化赋值:一:分配存储空间时的默认值(属于系统自动分配);二:程序员指定赋值:
- 使用 new 指定数组元素的值
dataType[] arrayName = dataType type[]{值 1,值 2,值 3,• • •,值 n};
简写:dataType[] arrayName = {值 1,值 2,值 3,...,值 n};
- 使用指定数组下标元素赋值
dataType[index] = value;
/**
* 数组的声明和初始化:
*/
class MyDemo14 {
public static void main(String[] args) {
/**
* 声明数组:声明的两种方式,建议使用第一种
* dataType[] arrayName; 数据类型[] 数组名;
* dataType arrayName[]; 数据类型 数组名[];
* 分配空间:在 Java 中可以使用 new 关键字来给数组分配空间。
* arrayName = new dataType[length]; 数组名 = new 数据类型[数组长度];
* 简化:dataType[] arrayName = new dataType[length]; 数据类型[] 数组名 = new 数据类型[数组长度];
* 初始化赋值:使用 new 指定数组大小后进行初始化
* dataType[] arrayName = new dataType[length];
* dataType[0] = value;
* dataType[1] = value;
* ...
* 初始化赋值:使用 new 指定数组元素的值
* dataType[] arrayName = new dataType[]{值 1,值 2,值 3,• • •,值 n};
* 简写:(静态初始化,必须在声明数组变量的同时进行初始化)
* dataType[] arrayName = {值 1,值 2,值 3,...,值 n};
*/
int[] score; // 声明一个整型数组变量
String[] name; // 声明一个字符串型数组变量
score = new int[10]; // 指定数组10大小,可以存储10个元素
name = new String[20]; // 指定数组20大小,可以存储20个元素
int[] num = new int[3]; // 创建一个长度为3的整型数组
num[0] = 1; // 初始化数组元素
num[1] = 2;
int[] num2 = new int[]{1,2,3,4}; // 创建一个元素为1,2,3,4的整型数组
int[] num3 = {1,2,3};
// num3 = {1,2,3,4,5,6}; // 报错,就不能修改数组大小
// num3 = new int[]{1,2,3,4,5,6}; // 但是可以重新声明数组大小
}
}
使用数组Array对象:
长度属性:
length
,下标元素:dataType[index]
。常用于循环:即数组的填充与迭代(遍历)。clone():创建并返回一个数组的副本。
/**
* 下标元素dataType[index],用于给其他变量赋值,或者给重新赋值该数组元素。
* 下标index的取值区间:[0,length-1],0表示第一个元素,length-1表示最后一个元素
*/
class MyDemo15 {
public static void main(String[] args) {
String[] name = new String[]{"小石头", "柳神", "火灵儿"};
int length = name.length; // 3
name[0] = "荒天帝";
name[2] = "火桑女";
for (int i = 0; i < name.length; i++) {
System.out.println(name[i]); // 荒天帝 柳神 火桑女
}
String[] clone = name.clone(); // ["荒天帝","柳神","火桑女"]
}
}
Arrays相关知识
工具类的作用:可以简化开发过程,提高代码的可读性和可维护性。
Arrays 类是一个工具类,用于操作和处理数组。它包含了一些常见的数组操作方法,如转换为字符串、排序、搜索、复制等。
这个 Arrays 类里均为 static 修饰的方法。static 修饰的方法可以直接通过类名调用,例如:Arrays.xxx()
Arrays.toString(array)
:将数组转换为字符串表示形式,方便打印和调试。
Arrays.sort(array)
:对数组进行升序排序。
Arrays.binarySearch(array, key)
:在已排序的数组中搜索指定元素的索引。
Arrays.copyOf(array, length)
:将原数组的指定长度的元素复制到新数组中。
Arrays.fill(array, value)
:将数组的所有元素设置为指定的值。
Arrays.equals()
:比较两个数组是否相等(内容相等:长度、大小、位置等等)
System.arraycopy
:数组的复制(可以在两个存在的数组之间进行复制)
需要注意的是,Arrays工具类的方法通常适用于基本数据类型和对象类型的数组。对于自定义的对象类型,需要保证对象实现了正确的比较器接口(如Comparable接口或自定义的比较器)才能进行排序和搜索操作。
/**
* 展示Arrays工具类方法:转换为字符串、排序、搜索、复制等。
*/
class MyDemo {
public static void main(String[] args) {
String[] name = new String[]{"荒天帝", "柳神", "火桑女"};
int[] ints = new int[]{5, 4, 3, 2, 1};
char[] input = {'b', 'y', 't', 'e'};
int[] ary1 = {1, 2, 3, 4, 5};
int[] ary2 = {1, 2, 3, 4, 5};
String[] num = {"1", "2", "3", "4", "5", "6", "7", "8", "9"};
/**
* 直接打印数组得到地址值
* 字符数组比较特殊,可以直接作为字符串输出
*/
System.out.println(name); // [Ljava.lang.String;@12edcd21
System.out.println(input); // byte
/**
* Arrays.toString(array):将数组转换为字符串表示形式,方便打印和调试。
*/
String s = Arrays.toString(name); // [小石头, 柳神, 火灵儿]
String s1 = Arrays.toString(ints); // [5, 4, 3, 2, 1]
/**
* Arrays.sort(array):对数组元素进行升序排序。
* 对于数字,按从小到大升序;
* 对于字符串,按首个字符大小进行升序,首字符大小相同时比较下一个字符大小
*/
Arrays.sort(ints); // [1, 2, 3, 4, 5]
Arrays.sort(name); // [柳神, 火桑女, 荒天帝]
/**
* Arrays.fill(array, value):将数组的所有元素设置为指定的值。
*/
// Arrays.fill(ints,1); // [1, 1, 1, 1, 1]
Arrays.fill(ints, 0, 3, 9); // [9, 9, 9, 4, 5]
/**
* Arrays.equals():比较两个数组是否相等(内容相等:长度、大小、位置等等)
* "==" 比的是是否为同一个对象
* equals 比较两个数组的内容是否相等
*/
System.out.println(ary1 == ary2); // false
System.out.println(Arrays.equals(ary1, ary2)); // true
/**
* Arrays.binarySearch(array, key):在已排序的数组中搜索指定元素的索引。
*/
int index = Arrays.binarySearch(num, "6"); // 5
/**
* Arrays.copyOf(T[] original,int newLength):拷贝数组
* original 原数组
* newLength 新数组的长度
* 其内部调用了System.arrayCopy()方法
* 如果新数组长度大于 original 的长度,超过的部分用系统默认值填充
* 如果新数组长度小于 original 的长度,只拷贝前几个数据
*/
int[] ary3 = Arrays.copyOf(ary1, 3); // [1, 2, 3]
int[] ary4 = Arrays.copyOf(ary1, ary1.length); // [1, 2, 3, 4, 5]
int[] ary5 = Arrays.copyOf(ary1, 8); // [1, 2, 3, 4, 5, 0, 0, 0]
/**
* System.arraycopy:数组的复制(可以在两个存在的数组之间进行复制)
* src 待复制的数组
* srcPos 待复制数组的起始位置
* dest 复制到的数组
* destPos复制到数组的起始位置
* length 需要复制的长度
*/
int[] line1 = {0,0,0,0,5,6,7,8};
int[] line2 = {1,2,3,4,0,0,0,0};
int[] line3 = {0,0,3,4,0,0,7,8};
System.arraycopy(line2,0,line1,0,4); // 赋值数组line1
System.out.println(Arrays.toString(line1)); // [1, 2, 3, 4, 5, 6, 7, 8]
System.arraycopy(line2,0,line3,0,2); // 赋值数组line3
System.out.println(Arrays.toString(line3)); // [1, 2, 3, 4, 0, 0, 7, 8]
System.arraycopy(line1,4,line3,4,2); // 赋值数组line3
System.out.println(Arrays.toString(line3)); // [1, 2, 3, 4, 5, 6, 7, 8]
}
}
自定义的比较器
Arrays相关知识补充
在Java中,可以使用Arrays类来创建集合,同时也可以使用Stream API来遍历数组。注意:通过Arrays.asList()方法创建的列表的大小是固定的,同数组一样,不能修改列表大小。
class MyDemo {
public static void main(String[] args) {
Integer[] ints = new Integer[]{1, 2, 3, 4, 5};
String[] str = new String[]{"cat", "rat"};
/**
* 创建集合
*/
List<Integer> list1 = Arrays.asList(ints);
List<String> list2 = Arrays.asList(str);
List<Integer> list3 = Arrays.asList(6, 7, 8, 9);
List<String> list4 = Arrays.asList("Tom", "Jerry");
/**
* 使用Stream API来遍历数组
*/
Arrays.stream(str).forEach(element -> {
System.out.println("遍历数组元素:" + element);
});
}
}
集合(Collection、List、Set、Map、Collections、Stream)
在编程时,可以使用数组来保存多个对象,但数组长度不可变化以及无法保存具有映射关系的数据。
Java提供了集合类来实现保存数量不确定的数据,以及保存具有映射关系的数据。Java 集合类型分为 Collection 和 Map,它们是 Java 集合的根接口,这两个接口又包含了一些子接口或实现类。
集合类和数组存储类型区别:数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量),而集合里只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集合里保存的是对象)。
Collection相关知识
Collection 接口是 List、Set 和 Queue 接口的父接口,通常情况下不被直接使用。Collection 接口定义了一些通用的方法,可以实现对集合的基本操作。定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。下列是Collection 接口中常用的方法:添加、删除、判断、其他(遍历、个数、转数组)
boolean add(E e)
:向集合中添加一个元素,如果集合对象被添加操作改变了,则返回 true。E 是元素的数据类型
boolean addAll(Collection c)
:向集合中添加集合 c 中的所有元素,如果集合对象被添加操作改变了,则返回 true。void clear()
:清除集合中的所有元素,将集合长度变为 0。
boolean remove(Object o)
:从集合中删除一个指定元素,当集合中包含了一个或多个元素 o 时,该方法只删除第一个符合条件的元素,该方法将返回 true。
boolean removeAll(Collection c)
:从集合中删除所有在集合 c 中出现的元素(相当于把调用该方法的集合减去集合 c)。如果该操作改变了调用该方法的集合,则该方法返回 true。
boolean retainAll(Collection c)
:从集合中删除集合 c 里不包含的元素(相当于把调用该方法的集合变成该集合和集合 c 的交集),如果该操作改变了调用该方法的集合,则该方法返回 true。boolean contains(Object o)
:判断集合中是否存在指定元素
boolean containsAll(Collection c)
:判断集合中是否包含集合 c 中的所有元素
boolean isEmpty()
:判断集合是否为空Iterator<E> iterator()
:返回一个 Iterator 对象,用于遍历集合中的元素
int size()
:返回集合中元素的个数
Object[] toArray()
:把集合转换为一个数组,所有的集合元素变成对应的数组元素。
List相关知识
List 是一个有序、可重复的集合,集合中每个元素都有其对应的顺序索引,可以通过索引来访问指定位置的集合元素。List 集合默认按元素的添加顺序设置元素的索引,第一个添加到 List 集合中的元素的索引为 0,第二个为 1,依此类推。List 实现了 Collection 接口,它主要有两个常用的实现类:ArrayList 类和 LinkedList 类。
ArrayList相关知识
ArrayList 类实现了可变数组的大小。ArrayList 类包含 Collection 接口中的所有方法还包括还包括 List 接口中提供的方法:查询、修改、子集合
E get(int index)
获取此集合中指定索引位置的元素,E 为集合中元素的数据类型
int index(Object o)
返回此集合中第一次出现指定元素的索引,如果此集合不包含该元
素,则返回 -1
int lastIndexOf(Object o)
返回此集合中最后一次出现指定元素的索引,如果此集合不包含该
元素,则返回 -1E set(int index, Eelement)
将此集合中指定索引位置的元素修改为 element 参数指定的对象。此方法返回此集合中指定索引位置的原元素。注意:当调用 List 的 set() 方法时,指定的索引必须是 List 集合的有效索引。例如集合长度为 4,就不能指定替换索引为 4 处的元素,也就是说这个方法不会改变 List 集合的长度。List<E> subList(int fromlndex, int tolndex)
返回一个新的集合,新集合中包含 fromlndex 和 tolndex 索引之间的所有元素。包含 fromlndex 处的元素,不包含 tolndex 索引处的元素
public class MyDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 添加元素
list.add("String1");
list.add("String2");
list.add("String3");
// 遍历集合
for (String s : list) {
System.out.println(s);
}
// 获取集合大小
int size = list.size();
// 获取指定索引的元素
String s = list.get(1);
// 获取首次出现指定元素的索引,如果此集合不包含该元素,则返回 -1
int string1 = list.indexOf("String1");
// 获取最后一次出现指定元素的索引,如果此集合不包含该元素,则返回 -1
int string2 = list.lastIndexOf("String2");
// 设置指定索引元素的值,返回未修改时的元素值
String string5 = list.set(2, "String5"); // String3
// 一个原集合的部分索引区间的子集合(前包后不包)
List<String> list1 = list.subList(0, 1);
// 添加集合
list1.addAll(list);
}
}
LinkedList相关知识
LinkedList 类采用链表结构保存对象,这种结构的优点是便于向集合中插入或者删除元素。需要频繁向集合中插入和删除元素时,使用 LinkedList 类比 ArrayList 类效果高,但是 LinkedList 类随机访问元素的速度则相对较慢。这里的随机访问是指检索集合中特定索引位置的元素。
LinkedList 类除了包含 Collection 接口和 List 接口中的所有方法之外,还特别提供了下列方法:查询、添加、删除
E getFirst()
:返回此集合的第一个元素
E getLast()
:返回此集合的最后一个元素void addFirst(E e)
:将指定元素添加到此集合的开头
void addLast(E e)
:将指定元素添加到此集合的末尾E removeFirst()
:删除此集合中的第一个元素
E removeLast()
:删除此集合中的最后一个元素
class MyDemo {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<String>();
// 添加
linkedList.addFirst("123");
linkedList.addLast("456");
linkedList.add("7890");
// 查询
String first = linkedList.getFirst(); // 123
String last = linkedList.getLast(); // 7890
// 删除
String s2 = linkedList.removeFirst(); // 123
String s1 = linkedList.removeLast(); // 7890
}
}
ArrayList 类和 LinkedList 类的区别
ArrayList:
底层数据结构是数组,实现了通过索引快速访问元素;
内部实现是动态扩容的,可以根据需要自动增长容量;
适用于随机访问和遍历,但不适用于频繁的插入和删除操作;
插入和删除元素时,需要移动其他元素,时间复杂度为O(n);
查询元素的时间复杂度为O(1)。
LinkedList:
底层数据结构是双向链表,实现了快速的插入和删除操作;
对于频繁的插入和删除操作,性能更好;
随机访问和遍历的性能较差,需要遍历链表找到元素;
插入和删除元素时,只需要修改链表的指针,时间复杂度为O(1);
查询元素的时间复杂度为O(n)。
综上所述,ArrayList适合于频繁的随机访问和遍历操作,而LinkedList适合于频繁的插入和删除操作。
时间复杂度相关知识
常见的时间复杂度有:常数O(1)、对数O(log n)、线性O(n)、线性对数O(n log n)、平方O(n^2)
O(1)
:常数时间复杂度。代表只需要计算一次既可以找到目标。例如访问数组中的某个元素。
O(log n)
:对数时间复杂度。代表每次查找排除一半,例如当数据增大256倍时,耗时只增大8倍。例如,二分查找算法。
O(n)
:线性时间复杂度。代表需要将所有数据遍历一次,数据量与耗时成正比。例如遍历一个数组。
O(n*log n)
:线性对数时间复杂度。表示算法的执行时间随着输入规模的增加而以n*log n的速度增长。例如,快速排序算法。
O(n^2)
:平方时间复杂度。代表需要双层遍历数据。例如冒泡排序。
耗时时长:常数O(1) < 对数O(log n) < 线性O(n) < 线性对数O(n log n) < 平方O(n^2)
Iterator迭代器相关知识
Iterator(迭代器)是一个接口,它的作用就是遍历容器的所有元素,也是java集合框架的成员,但它与 Collection 和 Map 系列的集合不一样,Collection 和 Map 系列集合主要用于盛装其他对象,而 Iterator 则主要用于遍历(即迭代访问)Collection 集合中的元素。
Iterator 接口里定义了如下 4 个方法:
boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回 true。
Object next():返回集合里的下一个元素。
void remove():删除集合里上一次 next 方法返回的元素。
void forEachRemaining(Consumer action):这是 Java 8 为 Iterator 新增的默认方法,该方法可使用 Lambda 表达式来遍历集合元素。
class MyDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> it = list.iterator(); // 创建 Iterator(迭代器)
while (it.hasNext()) { // 判断
Integer i = it.next(); // 查询集合里的下一个元素(从索引0开始,获取0,获取1,……)
System.out.println("迭代集合元素:" + i);
// it.remove(); // 删除集合中next()方法返回的元素
}
// System.out.println(list.get(0)); // 报 IndexOutOfBoundsException
// 迭代器只能使用一次,使用过后如需使用,只能再次创建迭代器
Iterator<Integer> it2 = list.iterator();
it2.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
}
}
集合遍历相关知识
集合遍历的多种方式及其使用场景
使用 for 循环:在性能要求不高的情况下,使用普通的 for 环遍历集合是一种简单方便的选择。我们也可以使用增强型的 for 循环(也称为"for-each"循环) 来遍历集合,但这种方式通常比普通的 for 循环慢一些。
使用迭代器:当我们需要在遍历集合的同时对其进行修改时,使用迭代器是一个不错的选择。迭代器可以让我们在遍历集合的同时删除或添加元素,而不需要了解底层实现细节。
使用 Listlterator:如果我们需要在遍历 List 集合的同时进行修改,并且希望能够在遍历过程中向前或向后移动遍历位置,那么使用 Listlterator 就是一个不错的选择。
使用并行流:如果我们需要对大型集合进行并行处理,那么使用并行流就是一种不错的选择。并行流使用多个线程来并行处理集合中的元素,可以提高处理速度。
使用 forEach 方法:Java 8 中引入了 forEach 方法,它允许我们使用 Lambda 表达式来遍历集合。这是一种简单方便的遍历方式,并且在性能方面也很优秀。
class MyDemo {
public static void main(String[] args) {
// 创建一个固定列表大小的集合
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
/**
* 使用for循环:普通的for循环、for-each循环来迭代访问集合元素
*/
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
for (Integer i : list) {
System.out.println(i);
}
/**
* 使用Iterator迭代器:hasNext()、next()、forEachRemaining
*/
Iterator<Integer> it = list.iterator();
while (it.hasNext()) { // 判断
int i = it.next(); // 查询集合里的下一个元素(从索引0开始,获取0,获取1,……)
System.out.println(i);
}
Iterator<Integer> it2 = list.iterator();
it2.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
/**
* 调用 forEach()方法遍历集合
*/
list.forEach(System.out::println);
list.forEach(obj -> System.out.println("迭代集合元素:" + obj));
/**
* 使用ListIterator向前向后遍历
*/
ListIterator<Integer> iterator1 = list.listIterator();
while (iterator1.hasNext()) { // 向后遍历
int i = iterator1.next();
System.out.println(i);
}
// 创建了一个从列表末尾开始的ListIterator
ListIterator<Integer> iterator2 = list.listIterator(list.size());
while (iterator2.hasPrevious()) { // 向前遍历
int i = iterator2.previous();
System.out.println(i);
}
/**
* 使用并行流遍历:并行流使用多个线程来并行处理集合中的元素,可以提高处理速度。
*/
// 创建一个并行流(将parallel标志指定为true表示创建并行流)
Stream<Integer> parallelStream = list.parallelStream();
// 查看流是否支持并行遍历
System.out.println("流是否支持并行遍历: " + parallelStream.isParallel());
// 使用 forEach() 方法遍历并行流
parallelStream.forEach(element -> System.out.println("迭代集合元素:" + element));
}
}
ListIterator相关知识补充
我们还可以使用 ListIterator 来修改、添加、删除集合中的元素
class MyDemo {
public static void main(String[] args) {
// 创建一个可变列表类型:
// 为了能够添加删除元素,这里我们使用的是 ArrayList(或其他可变列表类型)。
// 因为 Arrays.asList 方法创建的列表的大小是固定的,因此我们不能使用 add 或 remove 方法更改列表的大小。否则将抛出 UnsupportedOperationException 异常。
/**
* 创建一个可变列表类型:
* 为了能够添加删除元素,这里我们使用的是 ArrayList(或其他可变列表类型)。
* 因为 Arrays.asList 方法创建的列表的大小是固定的,不能更改列表的大小
* 使用 add 或 remove 方法更改列表的大小,将抛出 UnsupportedOperationException 异常。
*/
List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3"));
ListIterator<String> iterator = list.listIterator();
while (iterator.hasNext()) {
String name = iterator.next();
if (name.equals("1")) {
iterator.set("9"); // 修改元素
} else if (name.equals("3")) {
iterator.add("4"); // 在 3 之后添加元素
} else {
iterator.remove(); // 删除元素
}
}
System.out.println(list); // [9, 3, 4]
}
}
Set相关知识
Set是一个无序、不重复的集合,并且最多只允许包含一个 null 元素。
- 常见的Set实现类有:
HashSet:基于哈希表实现,不保证元素的顺序,允许使用null元素。
TreeSet:基于红黑树实现,按照元素的自然顺序或者指定的比较器进行排序,不允许使用null元素。
LinkedHashSet:基于哈希表和链表实现,有序集合,按照插入顺序排序,允许使用null元素。- Set集合的特点有:
不允许包含重复的元素,如果试图插入重复元素,插入操作将被忽略。
Set集合没有提供访问指定位置元素的方法,因为元素是无序的。
Set集合允许使用null元素,但是对于TreeSet来说,插入null元素将抛出NullPointerException异常。- Set集合常用的方法有:添加、删除、查询、判断
添加元素:add(Object obj)方法用于向Set集合中添加元素。
删除元素:remove(Object obj)方法用于从Set集合中删除指定元素。
清空集合:clear()方法用于清空Set集合中的所有元素。
获取集合大小:size()方法用于获取Set集合中元素的个数。
判断元素是否存在:contains(Object obj)方法用于判断Set集合中是否包含指定元素。- 使用Set集合时需要注意的问题包括:1、对于添加到Set集合中的自定义类对象,需要正确实现hashCode()和equals()方法,以保证元素的唯一性。2、在使用TreeSet时,需要确保元素实现了Comparable接口或者传入了指定的比较器。3、Set集合不保证元素的顺序,如果需要按照特定的顺序访问元素,可以转换为List集合再进行操作。4、对于大规模的数据集合,使用HashSet的性能通常比TreeSet更好。
HashSet相关知识
HashSet类实现了Set接口, 其底层其实是包装了一个HashMap去实现的。HashSet采用HashCode算法来存取集合中的元素,因此具有比较好的读取和查找性能。HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时就是使用这个实现类。
HashSet 具有以下特点:元素排列无序、HashSet 不是同步的、集合元素值可以是null
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据该 hashCode 值决定该对象在 HashSet 中的存储位置。如果有两个元素通过 equals() 方法比较返回的结果为 true,但它们的 hashCode 不相等,HashSet 将会把它们存储在不同的位置,依然可以添加成功。也就是说,两个对象的 hashCode 值相等且通过 equals() 方法比较返回结果为 true,则 HashSet 集合认为两个元素相等。
class MyDemo {
public static void main(String[] args) {
HashSet<Integer> in = new HashSet<>();
in.add(1); // 添加元素
in.add(1); // 重复添加,后添加的元素值会覆盖前面添加的元素值。确保元素不重复
in.add(999);
boolean contains = in.contains(1); // 判断元素是否存在 true
for (int i : in) {
System.out.println(i); // 1 999
}
}
}
TreeSet相关知识
TreeSet 类同时实现了 Set 接口和 SortedSet 接口。SortedSet 接口是 Set 接口的子接口,可以实现对集合进行自然排序,因此使用 TreeSet 类实现的 Set 接口默认情况下是自然排序(升序排序)。在使用自然排序时只能向 TreeSet 集合中添加相同数据类型的对象,否则会抛出 ClassCastException 异常。
TreeSet 只能对实现了 Comparable 接口的类对象进行排序,jdk类库中实现Comparable 接口的类有:
Byte、Short、Integer、Long 、Float、Double、BigDecimal、Biglnteger、Character、String
因为 TreeSet 中的元素是有序的,所以 TreeSet 还提供了查询、截取:
first()、last()、headSet()、subSet()、tailSet()
/**
* E first() 返回此集合中的第一个元素
* E last() 返回此集合中的最后一个元素
* E poolFirst() 获取并移除此集合中的第一个元素
* E poolLast() 获取并移除此集合中的最后一个元素
* SortedSet<E> headSet(E fromElement) 返回一个新的集合,包含小于该元素的所有对象,不包含该对象
* SortedSet<E> subSet(E fromElement) 返回一个新的集合,两者之间,前包后不包。
* SortedSet<E> tailSet(E fromElement) 返回一个新的集合,包含大于该元素的所有对象,包含该对象。
*/
class MyDemo {
public static void main(String[] args) {
TreeSet<Double> dou = new TreeSet<>();
for (int i = 0; i < 100; i++) {
dou.add(i * 1.0);
}
// 查询不及格的学生
SortedSet<Double> doubles = dou.headSet(60.0); // [0.0 , 60.0)
// 查询良好的学生
SortedSet<Double> doubles1 = dou.subSet(60.0, 90.0); // [60.0 , 90.0)
// 查询优秀的学生
SortedSet<Double> doubles2 = dou.tailSet(90.0); // >= 90.0
for (double v : doubles) {
System.out.print(v + "\t");
}
System.out.println();
for (double v : doubles1) {
System.out.print(v + "\t");
}
System.out.println();
for (double v : doubles2) {
System.out.print(v + "\t");
}
Double aDouble = dou.pollFirst(); // 0.0
Double aDouble1 = dou.pollLast(); // 99.0
Double first = dou.first(); // 1.0
Double last = dou.last(); // 98.0
}
}
LinkedHashSet相关知识
Map相关知识
Map 是一种键值对(key-value)集合,其中键key不重复、值value可重复。集合中的每一个元素都是一组键值对,包含一个键对象和一个值对象。其存在单向一对一关系,可用于保存具有映射关系的数据。
Map 集合里保存着两组值,一组值用于保存 Map 里的 key,另外一组值用于保存 Map 里的 value,key 和 value 都可以是任何引用类型的数据。
常见的 Map 实现类有:
HashMap:按哈希算法来存取键对象,
TreeMap:可以对键对象进行排序。
LinkedHashMap:
Hashtable:Map 接口中提供的常用方法:添加、查询、判断、删除、其他
V put(K key, V value)
:向 Map 集合中添加键值对。如果存在相同的key值,新的键值对会覆盖旧的键值对
void putAll(Map m)
:将指定 Map 中的 key-value 对复制到本 Map 中。Set keySet()
:获取 Map 集合中所有key对象的 Set 集合
V get(Object key)
:获取 Map 集合中指定key所对应的value值。V 表示值的数据类型boolean containsKey(Object key)
:查询 Map 中是否包含指定的 key,如果包含则返回 true。V remove(Object key)
:从 Map 集合中删除 key 对应的键值对,返回 key 对应的 value。如果该 key 不存在,则返回 nullMap 集合最典型的用法就是成对地添加、删除 key-value 对,接下来即可判断该 Map 中是否包含指定 key,也可以通过 Map 提供的 keySet() 方法获取所有 key 组成的集合,进而遍历 Map 中所有的 key-value 对。
class MyDemo {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
/**
* 添加键值对、获取key集合、遍历键值对、判断key是否存在、删除键值对
*/
for (int i = 0; i < 20; i++) {
hashMap.put(i, "demo" + i); // 添加键值对
}
if(hashMap.containsKey(10)){ // 判断是否包含指定的 key
hashMap.remove(10);// 删除 key 对应的键-值对
}
Set set = hashMap.keySet(); // 获取所有key集合
for (Object s : set) {
Object val = hashMap.get(s); // 根据key值获取value值
System.out.println("键:" + s + ",值:" + val); // 遍历键值对
}
/**
* 其他:
* 获取所有键值对:entrySet()
* 获取所有value值:values()
* ...
*/
Set set2 = hashMap.entrySet(); // 返回所有键值对的Set集合
Collection values = hashMap.values(); // 返回所有value集合
}
}
TreeMap相关知识
TreeMap 类的使用方法与 HashMap 类相同,唯一不同的是 TreeMap 类可以对键对象进行排序。因为键对象是有序的,所以提供了查询、截取:
firstEntry()、lastEntry()、headMap()、subMap()、tailMap()
等功能。类似于HashSet和TreeSet之间的关系。
class MyDemo7 {
public static void main(String[] args) {
TreeMap treeMap = new TreeMap();
for (int i = 0; i < 20; i++) {
treeMap.put(i, "demo" + i); // 添加键值对
}
Map.Entry entry = treeMap.firstEntry(); // 获取第一个
Map.Entry entry1 = treeMap.lastEntry(); // 获取最后一个
SortedMap sortedMap = treeMap.headMap(6);
SortedMap sortedMap1 = treeMap.subMap(6, 13);
SortedMap sortedMap2 = treeMap.tailMap(13);
}
}
LinkedHashMap相关知识
Hashtable相关知识
遍历Map集合的几种方式
增强型for循环遍历map集合:通过entrySet()遍历entry、通过keySet()遍历key、通过values()遍历value、通过键找值遍历
class MyDemo {
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
for (int i = 0; i < 20; i++) {
map.put(i, "demo" + i); // 添加键值对
}
// 使用 entries 实现 Map 的遍历(最常见和最常用的)
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue());
}
// 遍历 key
for (Integer key : map.keySet()) {
System.out.println(key);
}
// 遍历 values
for (String value : map.values()) {
System.out.println(value);
}
// 通过键找值遍历
for (Integer key : map.keySet()) {
String value = map.get(key);
System.out.println("key:" + key + ",value:" + value);
}
}
}
Collections 关知识
Collections 类是 Java 提供的一个操作 Set、List 和 Map 等集合的工具类。Collections 类提供了许多操作集合的静态方法,借助这些静态方法可以实现集合元素的排序、查找替换和复制等操作。下面介绍 Collections 类中操作集合的常用方法:排序、查询、修改
swap()
:元素交换。交换指定索引的列表元素
sort()
:排序。对列表进行自然升序
reverse()
:列表反转。对列表进行反转
shuffle()
:元素重排列。对列表进行重新排列,洗牌
rotate()
:数组旋转。该方法对于移动子列表中的一个或者而多个元素也是有效的binarySearch()
:二分查找。在已排序升序的 List 中进行二分查找
max()/min()
:最值元素查找。max()查找最大值,min()查找最小值
frequency()
:指定元素查找其出现的次数
indexOfSubList()
:实现子列表匹配,返回第1次出现的下标,找不到返回 -1
lastIndexOfSubList()
:实现子列表匹配,返回最后1次出现的下标,找不到返回 -1fill()
:列表填充。用指定的元素 obj 填充所有列表元素
copy()
:元素复制。将源列表 src 中的元素复制到目的地列表 dest 中去
class MyDemo {
public static void main(String[] args) {
List<Integer> in = Arrays.asList(3, 5, 7, 9, 1, 4, 2);
System.out.println(in); // 默认调用toString()方法 // [3, 5, 7, 9, 1, 4, 2]
/**
* 排序:
* void swap(List<?> list, int i, int j):元素交换。交换指定索引的列表元素
* void sort(List<?> list):排序。对列表进行自然升序
* void reverse(List<?> list):列表反转。对列表进行反转
* void shuffle(List<?> list):元素重排列。对列表进行重新排列,洗牌
* void rotate(List<?> list,int distance):数组旋转。该方法对于移动子列表中的一个或者而多个元素也是有效的
*/
Collections.swap(in, 2, 5); // 交换 // [3, 5, 4, 9, 1, 7, 2]
Collections.sort(in); // 自然升序 // [1, 2, 3, 4, 5, 7, 9]
Collections.reverse(in); // 反转 // [9, 7, 5, 4, 3, 2, 1]
Collections.shuffle(in); // 洗牌 // [7, 4, 3, 2, 1, 5, 9]
Collections.sort(in);
Collections.rotate(in,3); // 旋转 // [5, 7, 9, 1, 2, 3, 4]
Collections.rotate(in.subList(3,7),-1); // 指定索引范围旋转 // [5, 7, 9, 2, 3, 4, 1]
/**
* 查询:
* int binarySearch(List<?> list,T key):二分查找。在已排序升序的 List 中进行二分查找
* int frequency(Collection<?> c, Object o):指定元素查找其出现的次数
* int indexOfSubList(List<?> source, List<?> target):实现子列表匹配,返回第1次出现的下标
* int lastIndexOfSubList(List<?> source, List<?> target):实现子列表匹配,返回最后1次出现的下标
* 补充:没有找到或者 target.size() > source.size() 都会返回-1
* Integer max(List<?> list):查找最大值
* Integer min(List<?> list):查找最小值
*/
Collections.sort(in);
int i = Collections.binarySearch(in, 3); // 查找元素索引,自然升序后才能使用。// 2
int frequency = Collections.frequency(in, 9); // 指定元素出现次数 // 1
int i1 = Collections.indexOfSubList(in, in.subList(3, 6)); // [4, 5, 7],从下标 3开始 // 3
int i2 = Collections.lastIndexOfSubList(in, in.subList(2, 5)); // [3, 2, 1],从下标 2开始 // 2
Integer max = Collections.max(in); // 9
Integer min = Collections.min(in); // 1
/**
* 修改:
* void fill(List<?> list,T obj):列表填充。用指定的元素 obj 填充所有列表元素
* void copy(List<?> dest, List<?> src):元素复制。
* 注意:dest.size() >= src.size() 如果不满足条件,报IndexOutOfBoundsException
*/
Collections.fill(in, 99); // in [99, 99, 99, 99, 99, 99, 99]
List<Integer> in2 = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Collections.copy(in2, in); // in2 [99, 99, 99, 99, 99, 99, 99, 8, 9]
}
}
补充Collections相关知识
不可变包装类、同步安全包装类、动态类型安全集合包装类、空集合、单例集合、杂项/其他
找到的两篇不错的博客:
https://www.cnblogs.com/wpbxin/p/12546913.html
https://blog.csdn.net/zch981964/article/details/130786215
Stream相关知识
找到一份写的非常不错的博客:Java8中Stream详细用法大全 - 森林木马 - 博客园 (cnblogs.com)
Stream相关概念
Stream是Java8引入的一个新的API,它提供了一种更便捷、高效的方式来处理集合和数组。是对集合对象功能的增强,提供了查找、筛选、 排序、映射、聚合等操作。其使用链式编程的编程风格和lambda表达式,简化了代码,提高了编程效率以及代码可读性。简而言之,就是一工具类。
Stream特点
1、不是数据结构,不会保存数据。
2、每个 Stream 只能执行一次,且不会修改原来的数据源(peek可以修改原来的数据源,可能是用于调试吧)
3、惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行终止操作的时候才会进行实际的计算。
创建Stream
既然是处理集合和数组的,那么就有方法将集合和数组转成Stream。流常见的创建方法如下:集合的stream()方法、Arrays中的stream()方法、Stream中的静态方法、其他
class MyDemo {
public static void main(String[] args) throws FileNotFoundException {
/**
* 使用Collection下的 stream() 和 parallelStream() 方法
*/
List<Integer> in = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream1 = in.stream();
Stream<Integer> stream2 = in.parallelStream();
/**
* 使用Arrays 中的 stream() 方法,将数组转成流
*/
IntStream stream3 = Arrays.stream(new int[10]);
Stream<Integer> stream4 = Arrays.stream(new Integer[10]);
/**
* 使用 Pattern.splitAsStream() 方法,将字符串分隔成流
*/
Pattern pattern = Pattern.compile(",");
Stream<String> stream5 = pattern.splitAsStream("a,b,c,d");
/**
* 使用Stream中的静态方法:of()、iterate()、generate()、builder()
*/
Stream<String> stream6 = Stream.of("1", "2", "3", "4");
Stream<Integer> stream7 = Stream.iterate(0, (x) -> x + 2).limit(6);
Stream<Double> stream8 = Stream.generate(Math::random).limit(2);
Stream<Object> stream9 = Stream.builder().add(1).add(2).add(3).build();
/**
* 使用 BufferedReader.lines() 方法,将每行内容转成流
*/
BufferedReader reader = new BufferedReader(new FileReader("F:\\test_stream.txt"));
Stream<String> lines = reader.lines();
}
}
Stream常用的中间操作方法
筛选
filter、distinct
、切片skip、limit
、映射map、flatMap、mapToXxx
、排序sorted
、消费peek
class MyDemo {
public static void main(String[] args) {
/**
* 创建集合
*/
List<Integer> in = Arrays.asList(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9);
List<String> strings = Arrays.asList("a,b,c", "1,2,3");
Cat c1 = new Cat("aa", 10);
Cat c2 = new Cat("bb", 20);
Cat c3 = new Cat("aa", 30);
Cat c4 = new Cat("dd", 40);
List<Cat> cats = Arrays.asList(c1, c2, c3, c4);
List<String> strings1 = Arrays.asList("aa", "gg", "ff", "dd");
/**
* 筛选、切片
* filter元素过滤、distinct元素去重、skip跳过n个元素、limit获取n个元素
*/
Stream<Integer> limit = in.stream().filter(s -> s > 2) // 过滤,同 SQL 中的 where
.distinct() // 去重,同 SQL 中的 distinct
.skip(2) // 跳过前两个元素
.limit(10); // 获取 10 个元素,如果不足10个,那有几个就获取几个。
limit.forEach(obj -> System.out.print(obj + " ")); // 5 6 7 8 9
System.out.println();
/**
* 映射
* map:接收一个Function函数作为参数,操作流中的每一个元素,返回操作后的新元素。
* flatMap:接收一个Function函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
* mapToXxx():接收ToXxxFunction表达式,操作流中的每一个元素,返回操作后的新元素。
*/
Stream<String> s1 = strings.stream().map(s -> s.replaceAll(",", "")); // 将每个元素转成一个新的且不带逗号的元素
Stream<String> s2 = strings.stream().flatMap(s -> {
//将每个元素转换成一个stream
String[] split = s.split(",");
return Arrays.stream(split);
});
IntStream s3 = Stream.of("1", "3").mapToInt(Integer::parseInt); // 返回的对象是IntStream,而不是Stream<Integer>
s1.forEach(obj -> System.out.print(obj + " ")); // abc 123
System.out.println();
s2.forEach(obj -> System.out.print(obj + " ")); // a b c 1 2 3
System.out.println();
/**
* 排序
* sorted():自然排序,流中元素需实现Comparable接口
* sorted(Comparator com):定制排序,自定义Comparator排序器
*/
Stream<String> sorted = strings1.stream().sorted(); // String 类自身已实现Comparable接口
Stream<Cat> sorted1 = cats.stream().sorted(
(o1, o2) -> {
if (o1.getName().equals(o2.getName())) {
return o1.getAge() - o2.getAge();
} else {
return o1.getName().compareTo(o2.getName());
}
} // 自定义排序:先按姓名升序,姓名相同则按年龄升序
);
sorted.forEach(obj -> System.out.print(obj + " ")); // aa dd ff gg
System.out.println();
sorted1.forEach(obj -> System.out.print(obj + " "));
System.out.println();
/**
* 消费
* peek:接收的是Consumer表达式,能得到流中的每一个元素。没有返回值。
* 该方法主要用于调试。
*/
cats.stream().peek(o -> o.setName("ha")).forEach(obj -> System.out.print(obj + " "));
}
}
@Data
@AllArgsConstructor
class Cat {
String name;
int age;
int type;
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
}
Stream常用的终止操作1:聚合、匹配
匹配
allMatch、noneMatch、anyMatch
、聚合count、sum、max、min
、求值findFirst、findAny
class MyDemo {
public static void main(String[] args) {
/**
* 创建集合
*/
List<Integer> in = Arrays.asList(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9);
Cat c1 = new Cat("aa", 10, 1);
Cat c2 = new Cat("bb", 20, 2);
Cat c3 = new Cat("cc", 30, 3);
List<Cat> cats = Arrays.asList(c1, c2, c3);
/**
* allMatch:接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false
* noneMatch:接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false
* anyMatch:接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false
*/
boolean allMatch = in.stream().allMatch(e -> e > 10); //false
boolean noneMatch = in.stream().noneMatch(e -> e > 10); //true
boolean anyMatch = in.stream().anyMatch(e -> e > 4); //true
/**
* count:返回流中元素的总个数
* max:返回流中元素最大值
* min:返回流中元素最小值
* sum:XxxStream的方法,求总和
*/
long count = in.size(); // 集合大小
Integer max = in.stream().max(Integer::compareTo).get();
Integer min = in.stream().min(Integer::compareTo).get();
long count1 = cats.stream().map(Cat::getName).count(); // 个数
Integer sumAge = cats.stream().mapToInt(Cat::getAge).sum(); // 总值
Integer maxAge = cats.stream().map(Cat::getAge).max(Integer::compare).get(); // 最值
Double averageAge = cats.stream().collect(Collectors.averagingDouble(Cat::getAge)); // 平均值
/**
* findFirst:返回流中第一个元素
* findAny:返回流中的任意元素
*/
Integer findFirst = in.stream().findFirst().get();
Integer findAny = in.stream().findAny().get();
}
}
Stream常用的终止操作2:Collectors类
转集合
toList、toSet、toMap、toCollection
、分组groupingBy、groupingByConcurrent
、分区partitioningBy
、聚合summarizingDouble
、分隔符连接joining
class MyDemo16 {
public static void main(String[] args) {
/**
* 创建集合
*/
Cat c1 = new Cat("aa", 10, 1);
Cat c2 = new Cat("bb", 20, 2);
Cat c3 = new Cat("cc", 30, 3);
Cat c4 = new Cat("dd", 40, 4);
List<Cat> cats = Arrays.asList(c1, c2, c3, c4);
/**
* 转List、转Set、转Map,注:key不能相同,否则报错
*/
List<Integer> collect1 = cats.stream().map(Cat::getAge).collect(Collectors.toList()); // 返回一个不可变集合
Set<Integer> collect2 = cats.stream().map(Cat::getAge).collect(Collectors.toSet()); // 返回一个不可变集合
Map<String, Integer> collect3 = cats.stream().collect(Collectors.toMap(Cat::getName, Cat::getAge)); // 返回一个不可变集合
List<Integer> collect4 = cats.stream().map(Cat::getAge).collect(Collectors.toCollection(ArrayList::new)); // 返回一个可变集合
Set<Integer> collect5 = cats.stream().map(Cat::getAge).collect(Collectors.toCollection(HashSet::new)); // 返回一个可变集合
/**
* String::length 函数式接口将字符串转换成它的长度
* Function.identity() 函数式接口则将字符串映射成它本身
* 合并函数 (s1, s2) -> s1 + "|" + s2,如果出现重复的键,则将对应的值进行合并,以 | 分隔。
*/
Stream<String> stream1 = Stream.of("a", "bb", "ccc", "ddd");
Map<Integer, String> collect6 = stream1.collect(Collectors.toMap(String::length, Function.identity(), (s1, s2) -> s1 + "|" + s2));
/**
* 分组:groupingBy()、groupingByConcurrent()
* groupingBy() 方法返回的是一个 Map 对象,其中键是分类键,值则是由该分类键对应的元素构成的列表。
*/
// 使用 String::length 函数式接口将字符串转换为它的长度,并将其作为分类键进行分组
Map<Integer, List<String>> collect7 = Stream.of("a", "bb", "ccc", "ddd").collect(Collectors.groupingBy(String::length));
// 使用长度作为分类键进行分组,然后对分组结果进一步处理:统计每个分组中元素的数量,使用 Collectors.counting()
Map<Integer, Long> collect8 = Stream.of("a", "bb", "ccc", "ddd").collect(Collectors.groupingBy(String::length, Collectors.counting()));
Map<Integer, List<Cat>> collect9 = cats.stream().collect(Collectors.groupingBy(Cat::getAge));
// 多重分组,先根据类型分再根据年龄分
Map<Integer, Map<Integer, List<Cat>>> typeAgeMap = cats.stream().collect(Collectors.groupingBy(Cat::getType, Collectors.groupingBy(Cat::getAge)));
// Collectors.groupingByConcurrent() 方法返回的是一个线程安全(concurrent)的 ConcurrentMap 对象。
List<String> list = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
ConcurrentMap<Integer, List<String>> map = list.parallelStream().collect(Collectors.groupingByConcurrent(String::length));
/**
* 分区:partitioningBy()
*/
//分成两部分,一部分大于10岁,一部分小于等于10岁
Map<Boolean, List<Cat>> partMap = cats.stream().collect(Collectors.partitioningBy(v -> v.getAge() > 10));
// 键为 true 的部分包含了所有长度大于等于 2 的字符串,键为 false 的部分包含了所有长度小于 2 的字符串。
Map<Boolean, List<String>> collect = Stream.of("a", "bb", "ccc", "ddd").collect(Collectors.partitioningBy(s -> s.length() >= 2));
/**
* 聚合操作:计数、最值、总值、平均值
*/
DoubleSummaryStatistics statistics = cats.stream().collect(Collectors.summarizingDouble(Cat::getAge));
System.out.println("count:" + statistics.getCount() + ",max:" + statistics.getMax() + ",sum:" + statistics.getSum() + ",average:" + statistics.getAverage());
/**
* 字符串分隔符连接
*/
String joinName = cats.stream().map(Cat::getName).collect(Collectors.joining(",", "(", ")"));
}
}
Stream常用的终止操作3:规约
规约操作
reduce
:有点类似累加操作
文件(File、RandomAccessFile、Paths、Path、Files、URL、URI)
File相关知识
在Java中 File 类是 java.io 包中唯一代表磁盘文件本身的对象,也就是说,如果希望在程序中操作文件和目录,则都可以通过 File 类来完成。File类代表文件和目录。File 类定义了一些方法来操作文件,如新建、删除、重命名文件和目录等文件系统的操作。File 类不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入/输出流。
创建和使用File
通过new 的方式创建File类:
new File()
,相关的方法有:判断、创建、查询、修改、删除注意点:new File()不是创建文件、文件夹,其仅仅是创建内存对象,用来描述一个文件或者文件夹。存在多种重载形式:
new File(String pathname)
、new File(String dir, String name)
、…在File类中,存在两个常量:
File.separator
:用来分隔同一个路径字符串中的目录,Windows 下指/
或者\\
File.pathSeparator
:指的是分隔连续多个路径字符串的分隔符,Windows 下指;
class MyDemo {
public static void main(String[] args) throws IOException {
File file = new File("D:\\myFile\\test\\myFile.txt"); // 本地存在该文件
File file1 = new File("D:" + File.separator + "myFile"); // 使用 File.separator 代替反斜杠\\
File file2 = new File("D:/myFile2/test2/myFile2.txt"); // 本地不存在文件
File file3 = new File("D:\\myFile2\\test2\\myFile.txt"); // 本地不存在文件
File file4 = new File("D:\\myFile2\\test2\\myFile3\\my"); // 本地不存在文件
File file5 = new File("D:\\myFile2\\test2\\myFile2", "myFile.txt"); // 本地不存在文件
// 判断
boolean b1 = file.exists(); // 文件是否存在
boolean b2 = file.isFile(); // 文件是否为普通文件
boolean b3 = file.isDirectory(); // 是否为文件路径
boolean b4 = file.isAbsolute(); // 是否为绝对路径名
boolean b5 = file.canWrite(); // 是否支持写入
// 创建
boolean b6 = file2.mkdir(); // 创建单层目录是否成功
boolean b7 = file2.mkdirs(); // 创建目录是否成功,创建当前目录时会连同创建所有不存在的父目录
boolean b8 = file3.createNewFile(); // 创建文件是否成功。
// 查询
String s1 = file.getName(); // 文件名 // myFile.txt
String s2 = file.getParent(); // 父目录名 // D:\myFile\test
String s3 = file.getAbsolutePath(); // 文件的绝对路径名 // D:\myFile\test\myFile.txt
String s4 = file.getPath(); // 相对路径 // D:\myFile\test\myFile.txt
String[] list = file2.list(); // 返回目录中所有文件和子目录名称组成的字符串数组,File对象仅限于目录,否则返回 null
long l = file.lastModified(); // 最后一次修改的时间
File parentFile = file.getParentFile(); // 父目录文件对象
File absoluteFile = file.getAbsoluteFile();
File[] files = file3.listFiles();
// 修改
boolean b9 = file2.renameTo(file4); // 目录位置转移
boolean b10 = file3.renameTo(file5); // 文件位置转移
// 删除
boolean delete = file.delete();
}
}
File类应用1:创建和删除文档
创建文档
createNewFile()
、创建目录mkdirs()
、删除文档delete()
、删除目录delete()
class MyDemo {
public static void main(String[] args) throws IOException {
File file = new File("D:/myFile2/myFile2.txt"); // 创建文件对象
// deleteFile(file); // 删除文件
createFile(file); // 创建文件
}
public static void createFile(File file) throws IOException {
File parentFile = file.getParentFile(); // 父目录对象
// parentFile.mkdirs();// 创建目录,如果目录已经存在,不做任何操作
// file.createNewFile();// 创建文件,如果文件已经存在,不做任何操作
// // 创建目录(不存在父目录)
// if (!parentFile.exists()) {
// parentFile.mkdirs();//创建目录
// }
// // 创建文件(不存在该文件)
// if (!file.exists()) {
// file.createNewFile();//创建文件
// }
if (!file.exists()) {
File dir = file.getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
file.createNewFile();
}
}
// 将给定的File对象所表示的文件或者目录删除
public static void deleteFile(File file) {
if (file == null || !file.exists()) {
throw new RuntimeException("文件或者目录不存在");
}
if (file.isFile()) {
file.delete();
return;
}
File[] files = file.listFiles();
for (File sub : files) {
deleteFile(sub); // 递归调用,删除子文件和子文件夹
}
// 子项删除完毕之后(删除目录的必要前提),最后删除空的文件夹
file.delete();
}
}
File类应用2:遍历目录
通过遍历目录可以在指定的目录中查找文件,或者显示所有的文件列表。File 类的 list()、listFiles()方法提供了遍历目录功能,该方法有如下两种重载形式:
String[] list()
该方法表示返回由 File 对象表示目录中所有文件和子目录名称组成的字符串数组,如果调用的 File 对象不是目录,则返回 null。list() 方法返回的数组中仅包含文件名称,而不包含路径。
String[] list(FilenameFilter filter)
该方法的作用与 list() 方法相同,不同的是返回数组中仅包含符合 filter 过滤器的文件和目录,如果 filter 为 null,则接受所有名称。使用时首先需要创建文件过滤器,该过滤器必须实现 java.io.FilenameFilter 接口,并在 accept() 方法中指定允许的文件类型。
class MyDemo {
public static void main(String[] args) throws IOException {
File file = new File("D:/myFile2/myFile2.txt");
File file1 = new File("D:/myFile2");
File file2 = new File("D:/myFile2/my/file.md");
createFile(file);
createFile(file2);
String[] strings = file1.list();
for (String s : strings) {
System.out.println(s);
}
String[] strings2 = file1.list(new ImageFilter());
for (String s : strings2) {
System.out.println(s);
}
File[] files = file1.listFiles();
for (File s : files) {
System.out.println(s.isFile() ? s.getName() + "\t=>\t" + "文件" : s.getName() + "\t=>\t" + "文件夹");
}
File[] files2 = file1.listFiles(new ImageFilter());
for (File s : files2) {
System.out.println(s.isFile() ? s.getName() + "\t=>\t" + "文件" : s.getName() + "\t=>\t" + "文件夹");
}
}
}
class ImageFilter implements FilenameFilter {
// 实现 FilenameFilter 接口
@Override
public boolean accept(File dir, String name) {
// 指定允许的文件类型
return name.endsWith(".txt") || name.endsWith(".bat");
}
}
相对路径和绝对路径
class MyDemo {
public static void main(String[] args) throws IOException {
/*
* 使用相对路径"."表示当前目录,这里的当前目录指的是当前程序所在项目的根目录
*/
File file1 = new File(".\\demo");
// .\\可以省略不写,默认就表示当前目录下
// 如果参数不是绝对路径,则以代码当前路径作为基本路径,在这个基本路径基础上加上相对路径
File file2 = new File("demo\\demo.txt");
// 创建目录、文件
boolean mkdirs = file1.mkdirs(); // 创建目录
boolean newFile = file2.createNewFile(); // 创建文件
String absolutePath = file2.getAbsolutePath(); // 返回文件绝对路径 // D:\...\demo\demo.txt
String path = file2.getPath(); // 返回文件相对路径 // demo\demo.txt
System.out.println(mkdirs);
System.out.println(newFile);
System.out.println(absolutePath);
System.out.println(path);
}
}
RandomAccessFile类相关知识
RandomAccessFile是Java提供的一个类,用于在文件中读取和写入数据。与其他输入输出流不同的是,RandomAccessFile支持随机访问文件,可以在文件中任意位置读取或写入数据。
可以通过new 的方式创建RandomAccessFile对象:
new RandomAccessFile()
。重载方法有二:new RandomAccessFile(String name, String mode);
,其底层调用的就是第二种重载方法:new RandomAccessFile(File file, String mode);
,创建对象的区别只在于使用的mode的模式不同。mode是指打开文件的模式,有固定的值:“r”、“rw”、“rws”、“rwd”。其区别具体如下:
“r”:只读模式,只能读取文件,不能写入数据。
“rw”:读写模式,既可以读取文件,也可以写入数据。如果文件不存在,则会创建新文件。
“rws”:读写模式,每次写入数据后都会立即将数据同步到底层存储设备。
“rwd”:读写模式,每次写入数据后都会立即将数据和元数据同步到底层存储设备。总结:具有读写功能、不属于流、可以任意位置读写、可以使对象具有操作权限(读写)
File file = new File("D:/myFile2/myFile2.txt");
RandomAccessFile r = new RandomAccessFile(file, "r");
RandomAccessFile rw = new RandomAccessFile(file, "rw");
RandomAccessFile rws = new RandomAccessFile(file, "rws");
RandomAccessFile rwd = new RandomAccessFile(file, "rwd");
方法展示
创建该类的对象时,会自动的打开该文件,如果文件不存在,会自动创建该文件(仅限于创建文件,不创建目录)。此时如果执行写入操作,会有两种情况:如果该文件无内容,执行写入操作。如果该文件有内容,执行写入操作,此时会覆盖原文件内容。如果不想覆盖原文件内容,可以将文件指针跳length()位置,此时执行的写入操作就是追加写操作。
总结:创建对象时,如果文件不存在则创建该文件。执行写入操作有两种情况;覆盖、追加
方法:写入、读取、指针
写入分为三类:写入基本数据类型writeXxx()
、写入字符串writeUTF()
、写入字节数组write()
读取分为四类:读取基本数据类型readXxx()
、读取字符串readUTF()
、读取字节数组read()
、读取行readLine()
指针相关的有: 设置文件指针偏移量seek(long pos)
、返回文件指针的当前位置getFilePointer()
**write()**有三种重载方式:write(int b)
、write(byte[] b)
、write(byte[]b, int off, int len)
**read()**也有三种重载方式:read(int b)
、read(byte[] b)
、read(byte[]b, int off, int len)
class MyDemo27 {
public static void main(String[] args) throws IOException {
File file = new File("D:/myFile2/myFile3.txt");
write(file); // 写入
read(file); // 读取
filePointer(file); // 指针
append(file); // 追加
copy(file, "D:/myFile2/myFile4.txt"); // 复制文件
copy1(file, "D:/myFile2/myFile5.txt"); // 复制文件
}
public static void write(File file) throws IOException {
RandomAccessFile rw = new RandomAccessFile(file, "rw");
rw.write('A');
rw.writeByte(777);
rw.writeShort(777);
rw.writeInt(985);
rw.writeLong(333L);
rw.writeFloat(12.2F);
rw.writeDouble(12.2);
rw.writeChar('中');
rw.writeBoolean(true);
rw.writeUTF("中国人");
rw.close();
}
public static void read(File file) throws IOException {
RandomAccessFile rw = new RandomAccessFile(file, "rw");
System.out.println(rw.read());
System.out.println(rw.readByte());
System.out.println(rw.readShort());
System.out.println(rw.readInt());
System.out.println(rw.readLong());
System.out.println(rw.readFloat());
System.out.println(rw.readDouble());
System.out.println(rw.readChar());
System.out.println(rw.readBoolean());
System.out.println(rw.readUTF());
rw.close();
}
public static void filePointer(File file) throws IOException {
RandomAccessFile rw = new RandomAccessFile(file, "rw");
long filePointer = rw.getFilePointer(); // 返回当前文件指针位置
System.out.println(filePointer);
rw.seek(3); // 移动文件读取指针到指定位置
System.out.println(rw.getFilePointer());
rw.close();
}
public static void append(File file) throws IOException {
RandomAccessFile rw = new RandomAccessFile(file, "rw");
rw.seek(rw.length());
rw.writeUTF("追加如下内容:xxx");
rw.close();
}
public static void copy(File file, String copyName) throws IOException {
RandomAccessFile r = new RandomAccessFile(file, "r"); // 源文件
RandomAccessFile rw = new RandomAccessFile(copyName, "rw"); // 复件文件
// 边读边写(从源文件中读一个字节往复件中写一个字节) 结束条件就是读到源文件的末尾
int d;
while ((d = r.read()) != -1) {
rw.write(d);
}
r.close();
rw.close();
}
public static void copy1(File file, String copyName) throws IOException {
RandomAccessFile r = new RandomAccessFile(file, "r"); // 源文件
RandomAccessFile rw = new RandomAccessFile(copyName, "rw"); // 复件文件
//边读边写 创建一个用于缓存的字节数组
byte[] buffer = new byte[1024 * 10]; // 10kb
int i;
while ((i = r.read(buffer)) != -1) {
rw.write(buffer, 0, i);
}
r.close();
rw.close();
}
}
Paths相关知识
Paths类是一个被final修饰的类,用于返回Path对象。
Path get(String... param)
,Path get(URI uri)
。路径(Path)是用来表示文件或目录位置的抽象概念。
Path path1 = Paths.get("A","B","C"); // 将多个路径参数拼接形成一个完成的路径
Path path2 = Paths.get("A"); // 相对路径。采用项目当前路径为基础路径,加上给定的相对路径
Path path3 = Paths.get("D:/myFile2/myFile3.txt"); // 绝对路径
Path相关方法
查询
int getNameCount()
:返回路径的名称元素数。
Path getName(int index)
:返回指定索引处的路径名。
Path getFileName()
:返回路径中的文件名部分。
Path getParent()
:返回路径的父路径。
Path toAbsolutePath()
:
Path getRoot()
:
Path normalize()
:
转换
URI toUri()
:
File toFile():
String toString()
:返回路径的字符串表示。
FileSystem getFileSystem()
:
判断
boolean startsWith(Path other)
:判断路径是否以指定路径开始。
boolean endsWith(Path other)
:判断路径是否以指定路径结束。
boolean isAbsolute()
:判断路径是否为绝对路径。
解析组合
Path resolve(Path other)
:
Path resolveSibling(Path other)
:
Path relativize(Path other)
:
Files类相关知识
在Java中,Files类是java.nio包中的一个工具类,用于操作文件和目录。它提供了一系列静态方法,可以用于创建、复制、移动、删除、重命名文件和目录,以及判断文件和目录的属性等操作。
URI、URL相关知识
Java网络编程-URI和URL - throwable - 博客园 (cnblogs.com)
Spring居然还提供了这么好用的URL工具类 - 知乎 (zhihu.com)
URI全称是Uniform Resource Identifier,也就是统一资源标识符,它是一种采用特定的语法标识一个资源的字符串表示。
URL全称是Uniform Resource Location,也就是统一资源位置。实际上,URL就是一种特殊的URI,它除了标识一个资源,还会为资源提供一个特定的网络位置,客户端可以通过它来获取URL对应的资源。
相关知识补充
常用的工具类:json包、io包
IO(InputStream、OutputStream、Reader、Writer)
在Java中,除了变量、数组、集合这几种可以读写数据,还提供了IO流用于读写数据。与变量数组集合这类主要用于存储和处理数据不同,IO流的优势是可以处理输入输出数据。IO流可以读取来自文件、网络、硬件、内存等不同来源的数据,也可以将数据写入到文件、网络、硬件、内存等不同目的地。
流可以理解为一个工具类,用于读写各种“设备”中的数据,比如文件、键盘等。根据读写操作的不同可以将流分为输入流和输出流。输入就是将数据从各种输入设备(包括文件、键盘等)中读取到内存中,输出则正好相反,是将数据写入到各种输出设备(比如文件、显示器、磁盘等)。例如键盘就是一个标准的输入设备,而显示器就是一个标准的输出设备,但是文件既可以作为输入设备,又可以作为输出设备。
数据流是 Java 进行 I/O 操作的对象,它按照不同的标准可以分为不同的类别。按照流的方向主要分为输入流和输出流。数据流按照数据单位的不同分为字节流和字符流。按照功能可以划分为节点流和处理流。
数据流的处理只能按照数据序列的顺序来进行,即前一个数据处理完之后才能处理后一个数据。数据流以输入流的形式被程序获取,再以输出流的形式将数据输出到其它设备。
InputStream相关知识
InputStream 类是字节输入流的抽象类,是所有字节输入流的父类。InputStream 类中所有方法遇到错误时都会引发 IOException 异常。该类常用方法:读取
int read()
、关闭void close()
、查询int available()
、跳过skip(long n)
、重复读取boolean markSupported()、void mark(int readLimit)、void reset()
。
其中读取有三个重载方法:read()
、read(byte[] b)
、read(byte[] b,int off,int len)
InputStream的子类有七个:FileInputStream文件输入流、PipedInputStream管道输入流、ObjectInputStream对象输入流、SequenceInputStream顺序输入流、ByteArrayInputStream字节数组输入流、StringBufferInputStream缓冲字符串输入流、FilterInputStream过滤器输入流。
其中FilterInputStream有子类三个:PushBackInputStream回压输入流、BufferedInputStream缓冲输入流、DataInputStream数据输入流
总结:文件、管道、对象、顺序、字节数组、缓冲字符串、过滤器(回压、缓冲、数据)
OutputStream相关知识
OutputStream 类是字节输出流的抽象类,是所有字节输出流的父类。用于以二进制的形式将数据写入目标设备。OutputStream 类提供了一系列跟数据输出有关的方法:写入
write()
、刷新flush()
、关闭close()
。其中写入有三个重载方法:
write()
、write(byte[] b)
、write(byte[] b,int off,int len)
。
OutputStream的子类有五个:FileOutputStream文件输出流、PipedOutputStream管道输出流、ObjectOutputStream对象输出流、ByteArrayOutputStream字节数组输出流、FilterOutputStream过滤器输出流。
其中FilterInputStream有子类三个:PrintStream打印输出流、BufferedOutputStream缓冲输出流、DataOutputStream数据输出流
总结:文件、管道、对象、字节数组、过滤器(打印、缓冲、数据)
Reader相关知识
Java 中的字符是 Unicode 编码,即双字节的,而 InputerStream 是用来处理单字节的,在处理字符文本时不是很方便。这时可以使用 Java 的文本输入流 Reader 类,该类是字符输入流的抽象类,即所有字符输入流的实现都是它的子类,该类的方法与 InputerSteam 类的方法类似。
Reader 类的常用子类:CharArrayReader字符数组输入流、StringReader字符串输入流、BufferedReader缓冲输入流、PipedReader管道输入流、InputStreamReader字节转字符输入流
其中InputStreamReader有子类:FileReader字符文件输入流总结:字符数组、字符串、缓冲、管道、字节转字符(字符文件)
Writer相关知识
Writer类的常用子类:CharArrayWriter字符数组输出流、StringWriter字符串输出流、BufferedWriter缓冲输出流、PipedWriter管道输出流、OutputStreamReader字节转字符输出流
其中OutputStreamReader有子类:FileWriter字符文件输出流总结:字符数组、字符串、缓冲、管道、字节转字符(字符文件)
线程(Thread、Runable、Callable、ExecutorService、CompletableFuture)
以下是创建多线程的常用方式以及其最早出现的时间和后续版本的维护情况:
1、继承Thread类:这是最早也是最基本的多线程实现方式。从Java 1.0版本开始就有了Thread类,后续版本中没有对其进行重大改动。
2、实现Runnable接口:在Java 1.0版本中就已经存在了Runnable接口,用于表示可以由线程执行的任务。后续版本中没有对其进行重大改动。
3、实现Callable接口和使用FutureTask类:Callable接口是在Java 1.5版本中引入的,用于表示具有返回值的任务。FutureTask类也是在Java 1.5版本中引入的,用于表示一个异步计算的结果。在后续版本中,对FutureTask类进行了一些改进,使其更加高效和易用。
4、通过创建线程池:线程池是在Java 1.5版本中引入的,用于管理和复用线程。在后续版本中,对线程池进行了一些改进和优化,使其更加高效和灵活。
5、使用CompletableFuture类:CompletableFuture类是在Java 8版本中引入的,用于实现异步编程和处理多个任务的结果。在后续版本中,对CompletableFuture类进行了一些改进和优化,使其更加高效和灵活。
总的来说,Java在后续版本中对多线程的实现方式进行了一些改进和优化,使其更加高效、易用和灵活。但这些改进并没有改变这些多线程实现方式的基本原理和用法。
Thread相关知识
sdf
继承Thread类创建线程
public class MyThreadDemo {
public static void main(String[] args) {
// 创建多个线程
MyThread myThread1 = new MyThread("线程1");
MyThread myThread2 = new MyThread("线程2");
MyThread myThread3 = new MyThread("线程3");
// 运行多个线程
myThread1.start();
myThread2.start();
myThread3.start();
}
}
/** 继承Thread类 */
class MyThread extends Thread{
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(this.name +" : "+ i);
}
}
}
Runable相关知识
sd
实现Runnable接口创建线程
public class MyRunAbleDemo {
public static void main(String[] args) {
// 创建多个线程
MyRunAble myRunAble1 = new MyRunAble("线程1");
MyRunAble myRunAble2 = new MyRunAble("线程2");
MyRunAble myRunAble3 = new MyRunAble("线程3");
// 运行多个线程
new Thread(myRunAble1).start();
new Thread(myRunAble2).start();
new Thread(myRunAble3).start();
}
}
/** 实现Runnable接口 */
class MyRunAble implements Runnable{
private String name;
public MyRunAble(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(this.name +" : "+ i);
}
}
}
Callable相关知识
sdf
实现Callable接口和使用FutureTask类
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyCallAbleDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallAble myCallAble1 = new MyCallAble("线程1");
MyCallAble myCallAble2 = new MyCallAble("线程2");
MyCallAble myCallAble3 = new MyCallAble("线程3");
FutureTask<String> futureTask1 = new FutureTask<>(myCallAble1);
FutureTask<String> futureTask2 = new FutureTask<>(myCallAble2);
FutureTask<String> futureTask3 = new FutureTask<>(myCallAble3);
new Thread(futureTask1).start();
new Thread(futureTask2).start();
new Thread(futureTask3).start();
System.out.println("线程1的返回结果:" + futureTask1.get());
System.out.println("线程2的返回结果:" + futureTask2.get());
System.out.println("线程3的返回结果:" + futureTask3.get());
}
}
/** 实现Callable接口 */
class MyCallAble implements Callable<String> {
private String name;
public MyCallAble(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(this.name + " : " + i + "花枝鼠");
}
return this.name + "花枝鼠";
}
}
ExecutorService相关知识
sf
通过创建线程池
public class MyExecutorService {
public static void main(String[] args) {
// 创建线程池,指定线程数量
ExecutorService executor = Executors.newFixedThreadPool(10);
// 提交多个任务给线程池执行
for (int i = 0; i < 5; i++) {
MyRunAble task = new MyRunAble("线程"+i);
executor.submit(task);
}
// 关闭线程池,线程池不再接受新的任务,但会继续执行已提交的任务直到所有任务完成。
executor.shutdown();
try {
// 等待所有任务执行完毕
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("All tasks are completed");
}
}
CompletableFuture相关知识
sdf
使用CompletableFuture类
在Java 8中,CompletableFuture类提供了三种创建CompletableFuture对象的方法:completedFuture(), supplyAsync()和runAsync()。下面是它们的区别:
CompletableFuture.completedFuture(T value):此方法创建一个已经完成的CompletableFuture,并将指定的值作为结果返回。这个CompletableFuture不会执行任何实际的异步操作,直接返回一个已经完成的结果。
CompletableFuture<String> future = CompletableFuture.completedFuture("Hello");
String result = future.get(); // 获取结果,立即返回 "Hello"
CompletableFuture.supplyAsync(Supplier supplier):此方法使用指定的Supplier对象创建一个CompletableFuture,并在异步线程中执行供应商的操作。它返回一个CompletableFuture对象,可以通过get()方法获取异步操作的结果。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 执行具体的供应商操作
return "Hello";
});
String result = future.get(); // 获取结果,阻塞直到异步操作完成,返回 "Hello"
CompletableFuture.runAsync(Runnable runnable):此方法使用指定的Runnable对象创建一个CompletableFuture,并在异步线程中执行可运行的操作。它返回一个CompletableFuture对象,表示异步操作的结果为空。
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("Hello"); // 执行具体的可运行操作
});
future.get(); // 阻塞直到异步操作完成
总结:completedFuture()创建一个已经完成的CompletableFuture,supplyAsync()执行一个有返回值的异步操作,runAsync()执行一个没有返回值的异步操作。它们都返回一个CompletableFuture对象,可以使用get()方法获取异步操作的结果。
使用supplyAsync()方法
CompletableFuture.supplyAsync()方法会在默认的ForkJoinPool线程池中执行任务。如果需要自定义线程池,可以使用supplyAsync(Supplier supplier, Executor executor)方法,将自定义的Executor对象传递给它。这样可以更灵活地控制并发执行的线程池设置。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class MyCompletableFutureSupplyAsync {
public static void main(String[] args) {
// 创建CompletableFuture对象,分别用于异步执行两个任务
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
// 执行第一个任务
for (int i = 0; i < 30; i++) {
System.out.println("1号线程 : " + i + "花枝鼠");
}
return "Result 1";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
// 执行第二个任务
for (int i = 0; i < 30; i++) {
System.out.println("2号线程 : " + i + "大耗子");
}
return "Result 2";
});
// 使用CompletableFuture的方法组合两个任务的结果
CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
// 在两个任务都完成后,对结果进行处理
System.out.println("Combined Result: " + result1 + ", " + result2);
return "Combined Result";
});
try {
// 获取组合任务的结果
String combinedResult = combinedFuture.get();
System.out.println("Final Result: " + combinedResult);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
使用runAsync()方法
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class MyCompletableFutureRunAsync {
public static void main(String[] args) {
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
// 执行第一个任务
for (int i = 0; i < 30; i++) {
System.out.println("1号线程 : " + i + "花枝鼠");
}
});
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
// 执行第二个任务
for (int i = 0; i < 30; i++) {
System.out.println("2号线程 : " + i + "大耗子");
}
});
CompletableFuture<Void> future3 = CompletableFuture.runAsync(() -> {
// 执行第三个任务
for (int i = 0; i < 30; i++) {
System.out.println("3号线程 : " + i + "狸花猫");
}
});
try {
/* 等待所有任务执行结束,并获取结果
* allOf()方法返回一个CompletableFuture对象,表示所有的任务都已经完成。
* 调用.get()方法来获取等待的结果。因为allOf()方法返回的是一个CompletableFuture对象, * 所以调用get()方法并不会返回具体的结果,而是等待所有的CompletableFuture对象都完成。
* 在这个例子中,.get()方法会阻塞当前线程。确保在获取结果之前,所有的CompletableFuture对象都已经完成。
*/
CompletableFuture.allOf(future1, future2, future3).get();
System.out.println("All tasks are completed");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
使用CompletableFuture.supplyAsync()方法来实现10条线程的并发执行
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureExample {
public static void main(String[] args) {
// 创建10个CompletableFuture对象,并在每个对象上调用supplyAsync()方法
CompletableFuture<String>[] futures = new CompletableFuture[10];
for (int i = 0; i < 10; i++) {
int taskId = i;
futures[i] = CompletableFuture.supplyAsync(() -> {
// 执行任务
return "Task " + taskId + " is completed";
});
}
// 存储每个CompletableFuture对象的运行结果
List<String> tasks = new ArrayList<>();
/* 等待所有任务执行结束,并获取结果
* CompletableFuture.allOf(futures)表示等待所有任务执行结束
* .thenRun(() -> { ... })表示在所有任务完成后执行一个操作
* 通过futures[i].get()方法获取每个CompletableFuture对象的结果
* .join()方法用于等待所有任务完成。调用join方法会阻塞当前线程,直到所有任务都完成。
*/
CompletableFuture.allOf(futures).thenRun(() -> {
for (int i = 0; i < 10; i++) {
try {
tasks.add(futures[i].get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}).join();
System.out.println(tasks.get(8)); // 打印第八个任务的运行结果
}
}
锁(synchronized、volatile、CAS、lock、LockSupport、StampedLock)
以下是创建锁的常用方式以及其最早出现的时间和后续版本的维护情况:
锁:synchronized、volatile、CAS、lock、ReentrantLock、ReentrantReadWriteLock、Condition、LockSupport、StampedLock
java1.0出现:synchronized、volatile
java1.5出现:CAS、lock、ReentrantLock、ReentrantReadWriteLock、Condition、LockSupport
java1.8出现:StampedLockjava1.5优化:volatile
java1.6优化:synchronized、CAS、lock、Condition、LockSupport总结:
java1.0版本出现锁,引入同步锁、实现可见性和有序性。
java1.5版本引入了可重入锁、读写锁、条件锁、锁支持
java1.6版本引入了偏向锁、轻量级锁、重量级锁、锁消除、锁粗化、锁膨胀
java1.8版本引入了乐观读锁、悲观写锁
synchronized
synchronized:这是 Java 中最早的同步机制之一,从 JDK 1.0 开始就有。最初的实现是通过在对象头中的 Mark Word 中设置锁标志位来实现的。后来,JDK 6 引入了偏向锁、轻量级锁和重量级锁的概念来优化 synchronized 的性能。
synchronized 是 Java 中的关键字,用于实现线程的同步。使用 synchronized 无需手动执行加锁和释放锁的操作,我们只需要声明 synchronized 关键字就可以了,JVM会帮我们自动的进行加锁和释放锁的操作。 synchronized 可用于修饰普通方法、静态方法和代码块
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MySynchronized {
public static void main(String[] args) {
// 创建线程池,指定线程数量
ExecutorService executor = Executors.newFixedThreadPool(10);
unlocked(executor); // 无锁
// locked(executor); // 普通方法加锁
// staticLocked(executor); // 静态方法加锁
// thisBlock(executor); // this代码块加锁
// classBlock(executor); // class代码块加锁
// 关闭线程池
executor.shutdown();
try {
// 等待所有任务执行完毕
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("All tasks are completed");
}
private int a;
private int i;
private static int j;
private int m;
private int n;
/**
* 无锁
*/
public void fun0() {
a++;
System.out.println("无锁,普通方法,当前a的值:" + a + ",执行时间:" + LocalDateTime.now());
try {
// 休眠 3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 普通方法加锁
*/
public synchronized void fun1() {
i++;
System.out.println("加锁,普通方法,当前i的值:" + i + ",执行时间:" + LocalDateTime.now());
try {
// 休眠 3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 静态方法加锁
*/
public synchronized static void fun2() {
j++;
System.out.println("加锁,静态方法,当前j的值:" + j + ",执行时间:" + LocalDateTime.now());
try {
// 休眠 3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* this代码块加锁
*/
public void fun3() {
synchronized (this) {
m++;
System.out.println("加锁,this代码块,当前m的值:" + m + ",执行时间:" + LocalDateTime.now());
try {
// 休眠 3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* class代码块加锁
*/
public void fun4() {
synchronized (MySynchronized.class) {
n++;
System.out.println("加锁,class代码块,当前n的值:" + n + ",执行时间:" + LocalDateTime.now());
try {
// 休眠 3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 无锁,
*
* @param executor
*/
public static void unlocked(ExecutorService executor) {
/**
* 测试结果如下:
* 无锁,普通方法,当前a的值:1,执行时间:2023-08-15T09:21:56.517
* 无锁,普通方法,当前a的值:1,执行时间:2023-08-15T09:21:56.517
* 无锁,普通方法,当前a的值:1,执行时间:2023-08-15T09:21:56.517
*/
executor.execute(() -> {
MySynchronized task = new MySynchronized();
task.fun0();
});
executor.execute(() -> {
MySynchronized task = new MySynchronized();
task.fun0();
});
executor.execute(() -> {
MySynchronized task = new MySynchronized();
task.fun0();
});
/**
* 测试结果如下:
* 无锁,普通方法,当前a的值:3,执行时间:2023-08-15T09:26:07.113
* 无锁,普通方法,当前a的值:1,执行时间:2023-08-15T09:26:07.113
* 无锁,普通方法,当前a的值:2,执行时间:2023-08-15T09:26:07.113
*/
MySynchronized task = new MySynchronized();
executor.execute(task::fun0);
executor.execute(task::fun0);
executor.execute(task::fun0);
}
/**
* 加锁普通方法。对于多个对象无效,对于单个对象生效
*
* @param executor
*/
public static void locked(ExecutorService executor) {
/**
* 测试结果如下:对于多个对象,加锁似乎不起效果
* 加锁,普通方法,当前i的值:1,执行时间:2023-08-15T09:22:53.997
* 加锁,普通方法,当前i的值:1,执行时间:2023-08-15T09:22:53.997
* 加锁,普通方法,当前i的值:1,执行时间:2023-08-15T09:22:53.997
*/
executor.execute(() -> {
MySynchronized task2 = new MySynchronized();
task2.fun1();
});
executor.execute(() -> {
MySynchronized task2 = new MySynchronized();
task2.fun1();
});
executor.execute(() -> {
MySynchronized task2 = new MySynchronized();
task2.fun1();
});
/**
* 测试结果如下:同一个对象,只允许一个线程在运行,加锁成功
* 加锁,普通方法,当前i的值:1,执行时间:2023-08-15T09:24:46.700
* 加锁,普通方法,当前i的值:2,执行时间:2023-08-15T09:24:49.715
* 加锁,普通方法,当前i的值:3,执行时间:2023-08-15T09:24:52.731
*/
MySynchronized task2 = new MySynchronized();
executor.execute(task2::fun1);
executor.execute(task2::fun1);
executor.execute(task2::fun1);
}
/**
* 加锁静态方法,多个线程只允许一个线程生效
*
* @param executor
*/
public static void staticLocked(ExecutorService executor) {
/**
* 测试结果如下:静态方法加锁,只允许一个线程访问,其他线程不允许访问
* 加锁,静态方法,当前j的值:1,执行时间:2023-08-15T09:27:24.133
* 加锁,静态方法,当前j的值:2,执行时间:2023-08-15T09:27:27.133
* 加锁,静态方法,当前j的值:3,执行时间:2023-08-15T09:27:30.142
*/
executor.execute(MySynchronized::fun2);
executor.execute(MySynchronized::fun2);
executor.execute(MySynchronized::fun2);
}
/**
* this代码块加锁,对于多个对象无效,对于一个对象生效效
*
* @param executor
*/
public static void thisBlock(ExecutorService executor) {
/**
* 测试结果如下:this代码块加锁,对于多个对象,无效
* 加锁,this代码块,当前m的值:1,执行时间:2023-08-15T09:28:26.208
* 加锁,this代码块,当前m的值:1,执行时间:2023-08-15T09:28:26.208
* 加锁,this代码块,当前m的值:1,执行时间:2023-08-15T09:28:26.208
*/
executor.execute(() -> {
MySynchronized task3 = new MySynchronized();
task3.fun3();
});
executor.execute(() -> {
MySynchronized task3 = new MySynchronized();
task3.fun3();
});
executor.execute(() -> {
MySynchronized task3 = new MySynchronized();
task3.fun3();
});
/**
* 测试结果如下:this代码块加锁,对于一个对象,有效
* 加锁,this代码块,当前m的值:1,执行时间:2023-08-15T09:29:03.488
* 加锁,this代码块,当前m的值:2,执行时间:2023-08-15T09:29:06.492
* 加锁,this代码块,当前m的值:3,执行时间:2023-08-15T09:29:09.507
*/
MySynchronized task3 = new MySynchronized();
executor.execute(task3::fun3);
executor.execute(task3::fun3);
executor.execute(task3::fun3);
}
/**
* class代码块加锁,多个线程只允许一个线程生效
*
* @param executor
*/
public static void classBlock(ExecutorService executor) {
/**
* 测试结果如下:class代码块加锁,对于多个对象,有效。区别:变量有static
* 加锁,class代码块,当前n的值:1,执行时间:2023-08-15T09:29:55.005
* 加锁,class代码块,当前n的值:2,执行时间:2023-08-15T09:29:58.006
* 加锁,class代码块,当前n的值:3,执行时间:2023-08-15T09:30:01.021
*
* 测试结果如下:class代码块加锁,对于多个对象,有效。区别:变量无static
* 加锁,class代码块,当前n的值:1,执行时间:2023-08-15T09:58:21.941
* 加锁,class代码块,当前n的值:1,执行时间:2023-08-15T09:58:24.957
* 加锁,class代码块,当前n的值:1,执行时间:2023-08-15T09:58:27.972
*/
executor.execute(() -> {
MySynchronized task4 = new MySynchronized();
task4.fun4();
});
executor.execute(() -> {
MySynchronized task4 = new MySynchronized();
task4.fun4();
});
executor.execute(() -> {
MySynchronized task4 = new MySynchronized();
task4.fun4();
});
/**
* 测试结果如下:class代码块加锁,对于一个对象,有效。
* 加锁,class代码块,当前n的值:1,执行时间:2023-08-15T09:33:31.778
* 加锁,class代码块,当前n的值:2,执行时间:2023-08-15T09:33:34.782
* 加锁,class代码块,当前n的值:3,执行时间:2023-08-15T09:33:37.797
*/
MySynchronized task4 = new MySynchronized();
executor.execute(task4::fun4);
executor.execute(task4::fun4);
executor.execute(task4::fun4);
/**
* 加锁,class代码块,当前n的值:1,执行时间:2023-08-15T10:37:24.850
* 加锁,class代码块,当前n的值:1,执行时间:2023-08-15T10:37:27.866
* 加锁,class代码块,当前n的值:1,执行时间:2023-08-15T10:37:30.881
*/
MySynchronized task5 = new MySynchronized();
MySynchronized task6 = new MySynchronized();
MySynchronized task7 = new MySynchronized();
executor.execute(task5::fun4);
executor.execute(task6::fun4);
executor.execute(task7::fun4);
}
}
小结:
在上述案例中,加锁分为了两种方式:给类实例加锁、给类加锁
给类实例加锁:即给当前对象加锁,每个对象都对应了一把锁。表现为给普通方法加锁和this加锁这两种加锁方式。多线程调用同一个对象方法时,只能一个线程运行,其他线程阻塞。多线程调用多个对象方法时,所有线程都能同一时间运行。局部生效
给类加锁:表现为给静态方法加锁和xxx.class加锁这两种加锁方式。多线程调用多个对象方法时,只能一个线程运行,其他线程阻塞。多线程调用同一个对象方法时,也只能一个线程运行,其他线程阻塞。这是全局生效的。
volatile(待完成)
volatile:这也是从 JDK 1.0 开始就存在的,它用来保证变量的可见性和有序性。在早期的实现中,volatile 是通过在变量的访问和修改操作前后插入内存屏障(Memory Barrier)来实现的。后来,JDK 5 引入的新的 volatile 实现方式,通过使用内存屏障和缓存一致性协议来保证对 volatile 变量的操作满足内存可见性和有序性,从而解决了之前 volatile 变量的一些问题。通过使用 CAS 机制来优化 volatile 变量的读写操作,可以避免使用锁的开销,提高并发性能。
指令重排序问题
面对指令重排序对可见性的调整,volatile 采用 Happens-Before 规则解决:
1、何原始执行顺序中,在 volatile 变量写指令之前的其他变量读写指令,在重新排序后,不可以被放到 volatile 写指令之后。
2、任何原始执行顺序中,在 volatile 变量读指令之后的其他变量读写指令,在重新排序后,不可以被放到 volatile 读指令之前。
有了以上两条 Happens-Before 规则,我们就避免了指令重排序对 volatile 可见性的影响。使用时:写的时候,将带volatile的变量放在最后位置,读的时候,将带volatile的变量放在最开头位置。写的时候,前面的指令不会排到volatile变量之后,读的时候,后面的指令不会排到volatile变量之前
代码演示(待完成)
CAS(待完成)
CAS:(Compare and Swap)机制是在Java中用于实现原子操作的一种机制。它最早是在JDK 1.5版本中引入的。CAS机制通过比较内存中的值与期望值,如果相等则将新值写入内存。CAS操作是原子的,保证了多线程环境下的线程安全性。CAS机制在多线程编程中具有高性能和高并发性的优势,因此在并发编程中被广泛应用。它提供了一种无锁的方式来实现线程安全,避免了锁带来的竞争和开销。
Lock(待完成)
Lock:Lock 接口是在 JDK 5 中引入的,它是对传统的 synchronized 关键字的一种替代方案。Lock 提供了更多的功能和灵活性,例如可重入锁、读写锁、条件变量等。Lock 的实现使用了更底层的同步机制,如 CAS(Compare and Swap)操作,来提高性能和并发度。在Java 6中,对Lock接口进行了性能优化。改进了锁的底层实现,提高了锁的性能和并发能力。
ReentrantLock(待完成)
ReentrantLock:这是 Lock 接口的实现类之一,它是在 JDK 5 中引入的。ReentrantLock 是一个可重入锁,它允许同一个线程多次获取锁而不会造成死锁。ReentrantLock 的实现使用了类似 synchronized 的方式,但提供了更多的高级特性,如公平锁和非公平锁的选择、可中断的获取锁等。
ReentrantReadWriteLock(待完成)
ReentrantReadWriteLock:这是 Lock 接口的另一个实现类,从JDK 1.5版本开始引入。它是一个可重入读写锁,包含一个读锁和一个写锁,多个线程可以同时获取读锁,但只有一个线程可以获取写锁。在读多写少的场景下,可以提高并发性能。早期的实现并没有太多的优化,主要是通过内部维护读锁和写锁的状态来实现读写的互斥。后来,随着对并发性能的要求越来越高,JDK 8 引入了基于 CAS 的 StampedLock,提供了更高效的读写锁的实现。
Condition(待完成)
Condition:是在 JDK 5 中引入的,它是 Lock 接口下的一个重要组件。Condition 是一个可重入读写锁,提供了更灵活的线程等待和唤醒机制,常与ReentrantLock搭配使用。通过 Condition,我们可以实现更复杂的线程间协作,例如生产者-消费者模型中的等待和通知机制。早期的实现并没有太多的优化,主要是通过内部的等待队列和通知机制来实现线程的等待和唤醒。后来,随着对线程协作性能的要求越来越高,JDK 6 引入了更高效的 Condition 实现方式,使用了更底层的同步机制来提高性能。
LockSupport(待完成)
LockSupport:是 Java 并发包中的一个工具类,用于线程的阻塞和唤醒操作。最早出现在 JDK 5 中,用于解决传统的线程阻塞和唤醒机制的问题。JDK 6 对其进行了优化,优化后的 LockSupport 使用了更底层的同步机制,如 CAS 操作和内存屏障,来实现线程的阻塞和唤醒。这种底层机制的使用可以减少上下文切换的次数,提高线程的性能和效率。
LockSupport 是 Java 并发包中的一个工具类,用于线程的阻塞和唤醒操作。它提供了 park() 和 unpark() 两个静态方法,分别用于阻塞和唤醒线程。
LockSupport 的最早出现时间是在 JDK 5 中。它的设计初衷是为了解决传统的线程阻塞和唤醒机制中的一些问题,如使用 wait() 和 notify() 方法时需要先获得对象的锁、notify() 只能随机唤醒一个等待线程等。
LockSupport 的优化主要体现在实现上。早期的实现方式是通过类似于信号量的方式实现线程的阻塞和唤醒。然而,这种方式会存在一些性能问题,如线程在阻塞和唤醒过程中需要频繁地进行上下文切换。为了提高 LockSupport 的性能,JDK 6 对其进行了优化。
优化后的 LockSupport 使用了更底层的同步机制,如 CAS 操作和内存屏障,来实现线程的阻塞和唤醒。这种底层机制的使用可以减少上下文切换的次数,提高线程的性能和效率。此外,LockSupport 还提供了一些高级功能,如支持线程的中断和超时等待。
总结:
LockSupport 是 Java 并发包中的一个工具类,用于线程的阻塞和唤醒操作。
LockSupport 最早出现在 JDK 5 中,用于解决传统的线程阻塞和唤醒机制的问题。
LockSupport 在 JDK 6 中进行了优化,使用了更底层的同步机制来提高性能,并提供了一些高级功能。
优化后的 LockSupport 使用了 CAS 操作和内存屏障等底层机制,减少上下文切换次数,提高线程性能和效率。
StampedLock(待完成)
StampedLock:是在 JDK 8 中引入的,它是基于 CAS 的读写锁的实现。StampedLock 提供了乐观读锁和悲观写锁的支持,可以在读多写少的情况下提供更高的并发性能。它使用了基于 CAS 的乐观读锁来避免线程的阻塞和唤醒,提高了并发性能。此外,StampedLock 还提供了一种机制来检测是否出现了写入冲突,以避免读线程陷入长时间的等待。
通信(javax.servlet包、javax.websocket包、javax.mail包、java.net包、)
javax.servlet包相关知识
Sevelet、JSP、JSTL
常见机制
反射、异常、泛型、序列化、注解、多线程、垃圾回收
反射(Reflection):Java的反射机制允许程序在运行时动态地获取对象的信息并操作对象。通过反射,可以在运行时检查类的结构、访问和修改对象的属性和方法,以及创建新的对象实例。
异常处理(Exception Handling):Java的异常处理机制允许程序在遇到异常情况时进行处理,避免程序崩溃。通过使用try-catch语句块,可以捕获并处理异常,保证程序的稳定性和可靠性。
序列化(Serialization):Java的序列化机制允许对象在网络传输或持久化存储时进行序列化和反序列化。通过将对象转换为字节流,可以将对象保存到文件中或通过网络传输,而不需要关心对象的内部结构。
泛型(Generics):Java的泛型机制允许在编译时指定类或方法的参数类型,提高代码的类型安全性和重用性。通过使用泛型,可以在编译时检查类型的一致性,并减少类型转换的需要。
注解(Annotations):Java的注解机制允许在代码中添加元数据信息,以实现更丰富的代码描述和编译时的静态检查。通过使用注解,可以在编译器和运行时对代码进行额外的处理和验证。
多线程(Multithreading):Java的多线程机制允许程序同时执行多个线程,实现并发编程。通过创建和管理多个线程,可以充分利用多核处理器的计算能力,提高程序的性能和响应性。
垃圾回收(Garbage Collection):Java的垃圾回收机制自动管理内存分配和释放。它通过跟踪对象的引用,识别不再使用的对象,并自动回收它们所占用的内存空间,减少了对开发者的内存管理负担。这些机制和特性是Java编程中常用的一些工具和技术,它们提供了更加灵活和强大的编程能力,帮助开发者编写高效、可靠和可维护的Java应用程序。