JavaSE
- 一、Java简介
- 二、Java开发环境
- 三、Java基础语法
- 四、Java注释
- 五、Java数据类型
- 六、Java变量类型
- 七、Java运算符
- 八、Java Scanner 类
- 九、Java控制结构
- 十、Java 数组
- 十一、面向对象基础
- 十二、面向对象进阶
- 十三、Object类详解
- 十四、面向对象高级
- 十五、Java异常处理
- 十六、常用类详解
- 十七、集合
- 十八、Java泛型
- 十九、多线程
- 二十、IO流
一、Java简介
1.1 什么是Java
Java是一种编程语言和平台。Java是一种高级,健壮,面向对象和安全的编程语言。
Java是由_Sun Microsystems_(现在是Oracle的子公司)在1995年开发的。JamesGosling_被称为Java的父亲。在Java之前,它的名字叫_Oak。由于Oak已经是一家注册公司,因此James Gosling和他的团队将Oak的名称更改为Java。
平台:运行程序的任何硬件或软件环境都称为平台。由于Java具有运行时环境(JRE)和API,因此称为平台。
1.2 Java平台/版本
Java SE(Java标准版)
它是一个Java编程平台。它包含Java编程API,例如java.lang,java.io,java.net,java.util,java.sql,java.math等。它包含诸如OOP,String,Regex,Exception,内部类,多线程, I / O流,网络,AWT,Swing,反射,收集等
Java EE(Java企业版)
它是一个企业平台,主要用于开发Web和企业应用程序。它建立在Java SE平台的顶部。它包括诸如Servlet,JSP,Web服务,EJB,JPA等主题。
Java ME(Java微型版)
这是一个微型平台,主要用于开发移动应用程序。
JavaFX
它用于开发丰富的Internet应用程序。它使用轻量级的用户界面API。
1.3 Java语言的特点
- 面向对象
面向对象就是Java语言的基础,也是Java语言的重要特性。面向对象是指以对象为基本单元,包含属性和方法。对象的状态用属性表达,对象的行为用方法表达。
面向对象技术使得应用程序的开发变得简单易用,节省代码。总之,Java语言是一个纯面向对象的程序设计语言。 - 跨平台性
Java 程序可以在不同的平台上运行,因为它们是在 Java 虚拟机(JVM)上运行的,Java 编译器将 Java 源代码编译成字节码,JVM 将字节码转换为特定于平台的机器代码。这就是Java的能够Write once, run anywhere(一次编译,到处运行)的原因。 - 简单性
Java的设计简洁清晰,摒弃了许多容易导致错误的特性,如指针和操作符重载 - 健壮性
Java 提供了一些功能,如垃圾回收和异常处理,有助于编写健壮的程序,防止内存泄漏和程序崩溃。 - 安全性
Java 提供了安全管理机制,如类加载器和安全管理器,可以确保 Java 应用程序在安全的环境中运行。 - 平台无关性
Java 语言和平台是独立的,Java 程序只需编写一次,就可以在任何支持 Java 的平台上运行。 - 多线程支持
Java 内置了对多线程编程的支持,使得编写多线程程序变得更加简单和高效。 - 高性能
虽然 Java 是一种解释性语言,但通过即时编译器(JIT)和优化技术,Java 程序可以获得接近原生代码的性能。 - 分布式计算
Java 内置了对网络和分布式计算的支持,可以轻松地编写分布式应用程序和网络服务。 - 动态性
Java 支持反射和动态代理等特性,使得程序可以在运行时动态地加载、链接和运行类。
二、Java开发环境
2.1 JVM,JRE和JDK的关系
JVM(Java Virtual Machine)
是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。JVM包含在jdk中。
JRE(Java Runtime Environment)
包括Java虚拟机和Java程序所需的核心类库等(JRE = JVM + Jav核心类库)。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包。如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
JDK(Java Development Kit)
是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等。
2.2 设置Windows Java环境的步骤
-
下载JDK(https://www.oracle.com/java/technologies/downloads/#java8-windows)
-
运行安装程序
-
转到windows设置-高级系统设置
-
在“高级系统设置”选项下,单击“ 环境变量”,如下所示。
-
在 “系统变量” 中设置 3 项属性,JAVA_HOME、PATH、CLASSPATH(大小写无所谓),若已存在则点击"编辑",不存在则点击"新建"。变量设置参数如下:
变量名 | 变量值 | 备注 |
---|---|---|
JAVA_HOME | C:\Program Files\Java\jdk1.8.0_251 | 根据自己的java路径 |
CLASSPATH | .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar; | 注意最前面有个"." |
Path | %JAVA_HOME%\bin;%JAVA_HOME%\jre\bin; |
- 打开命令提示符并键入
javac -version
输出Java版本号即为安装成功
2.3 设置Linux Java环境的步骤
在Linux中使用终端安装Java。对于Linux,我们将安装OpenJDK。OpenJDK是Java编程语言的免费开源实现。
-
转到应用程序->附件->终端。
-
键入命令如下
sudo apt-get install openjdk-8-jdk
-
配置环境变量
//1.配置JAVA_HOME 根据实际安装路径设置 export JAVA_HOME = /usr/lib/jvm/java-8-openjdk //2.配置PATH 根据实际安装路径设置 export PATH = $PATH:/usr/lib/jvm/java-8-openjdk/bin
三、Java基础语法
一个 Java 程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作。
- 对象:对象是对象是类的一个实例,有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
- 类:类是一个模板,它描述一类对象的行为和状态。
- 方法:方法就是行为,一个类可以有很多方法。逻辑运算、数据修改以及所有动作都是在方法中完成的。
- 实例变量:每个对象都有独特的实例变量,对象的状态由这些实例变量的值决定。
3.1 第一个Java程序
public class HelloWorld {
/* 第一个Java程序
* 它将输出字符串 Hello World
*/
public static void main(String[] args) {
System.out.println("Hello World"); // 输出 Hello World
}
}
3.2 基本语法
- 大小写敏感:Java 是大小写敏感的,这就意味着标识符 Hello 与 hello 是不同的。
- 类名:对于所有的类来说,类名的首字母应该大写。如果类名由若干单词组成,那么每个单词的首字母应该大写,例如 MyFirstJavaClass ,这种命名方式称为大驼峰命名法 。
- 方法名:所有的方法名都应该以小写字母开头。如果方法名含有若干单词,则后面的每个单词首字母大写,例如 myFirstJavaFunction ,这种命名方式称为大驼峰命名法。
- 源文件名:源文件名必须和类名相同。当保存文件的时候,你应该使用类名作为文件名保存(切记 Java 是大小写敏感的),文件名的后缀为 .java。(如果文件名和类名不相同则会导致编译错误)。
- 主方法入口:所有的 Java 程序由 public static void main(String[] args) 方法开始执行。
3.3 Java 标识符
Java 所有的组成部分都需要名字。类名、变量名以及方法名都被称为标识符。
关于 Java 标识符,有以下几点需要注意:
- 所有的标识符都应该以字母(A-Z 或者 a-z),美元符($)、或者下划线(_)开始
- 首字符之后可以是字母(A-Z 或者 a-z),美元符($)、下划线(_)或数字的任何字符组合
- 关键字不能用作标识符
- 标识符是大小写敏感的
- 合法标识符举例:age、$salary、_value、__1_value
- 非法标识符举例:123abc、-salary
3.4 Java修饰符
像其他语言一样,Java可以使用修饰符来修饰类中方法和属性。主要有两类修饰符:
- 访问控制修饰符 : default, public , protected, private
- 非访问控制修饰符 : final, abstract, static, synchronized
3.5 Java 变量
Java 中主要有如下几种类型的变量
- 局部变量
- 类变量(静态变量)
- 成员变量(非静态变量)
3.6 Java 数组
数组是储存在堆上的对象,可以保存多个同类型变量。
3.7 Java 枚举
Java 5.0引入了枚举,枚举限制变量只能是预先设定好的值。使用枚举可以减少代码中的 bug。
例如,我们为果汁店设计一个程序,它将限制果汁为小杯、中杯、大杯。这就意味着它不允许顾客点除了这三种尺寸外的果汁。
class FreshJuice {
enum FreshJuiceSize{ SMALL, MEDIUM , LARGE }
FreshJuiceSize size;
}
public class FreshJuiceTest {
public static void main(String[] args){
FreshJuice juice = new FreshJuice();
juice.size = FreshJuice.FreshJuiceSize.MEDIUM ;
}
}
📌
注意:枚举可以单独声明或者声明在类里面。方法、变量、构造函数也可以在枚举中定义。
3.8 Java 关键字列举
四、Java注释
被注释的内容,不会被JVM解释执行。注释可以提高代码可读性,注释是良好的编程习惯,它们帮助程序员更容易地理解代码的用途和功能。
4.1 单行注释
基本格式:
//注释内容
4.2 多行注释
/*
注释内容第一行
注释内容第二行
*/
📌
多行注释中不允许有多行注释嵌套
4.3 文档注释
文档注释的内容可以被JDK提供的工具javadoc所解析,生成一套以网页文件形式体现的该程序的说明文档,一般写在类上。
基本格式:
/**
* @author Corgi
* @version 1.0
*/
其中的@符号开头的是javadoc中的标签
javadoc -d 文件夹路径 -xx -yy Demo3.java
-d:将生成的文档放在文件夹路径中
-xx -yy:需要指明用到的javadoc标签
4.4 javadoc 标签
标签 | 描述 | 示例 |
---|---|---|
@author | 标识一个类的作者 | @author description |
@deprecated | 指名一个过期的类或成员 | @deprecated description |
{@docRoot} | 指明当前文档根目录的路径 | Directory Path |
@exception | 标志一个类抛出的异常 | @exception exception-name explanation |
{@inheritDoc} | 从直接父类继承的注释 | Inherits a comment from the immediate surperclass. |
{@link} | 插入一个到另一个主题的链接 | {@link name text} |
{@linkplain} | 插入一个到另一个主题的链接,但是该链接显示纯文本字体 | Inserts an in-line link to another topic. |
@param | 说明一个方法的参数 | @param parameter-name explanation |
@return | 说明返回值类型 | @return explanation |
@see | 指定一个到另一个主题的链接 | @see anchor |
@serial | 说明一个序列化属性 | @serial description |
@serialData | 说明通过writeObject( ) 和 writeExternal( )方法写的数据 | @serialData description |
@serialField | 说明一个ObjectStreamField组件 | @serialField name type description |
@since | 标记当引入一个特定的变化时 | @since release |
@throws | 和 @exception标签一样. | The @throws tag has the same meaning as the @exception tag. |
{@value} | 显示常量的值,该常量必须是static属性。 | Displays the value of a constant, which must be a static field. |
@version | 指定类的版本 | @version info |
五、Java数据类型
5.1 基本数据类型
基本类型 | 大小 | 最小值 | 最大值 | 默认值 | 包装器类型 | bei’zhu |
---|---|---|---|---|---|---|
byte | 8 bit / 1个字节 | -128 | 127 | 0 | Byte | |
short | 16 bit / 2个字节 | -2^15 | 2^15-1 | 0 | Short | |
int | 32 bit / 4个字节 | -2^31 | 2^31-1 | 0 | Integer | |
long | 64 bit / 8个字节 | -2^63 | 2^63-1 | 0L | Long | 声明long型常量须在后面加’l‘或‘L’ |
float | 32 bit / 4个字节 | IEEE754 | IEEE754 | 0.0f | Float | 单精度,声明float型常量须在后面加’f‘或‘F’ |
double | 64 bit / 8个字节 | IEEE754 | IEEE754 | 0.0d | Double | shuang’jing’du |
boolean | - | - | - | false | Boolean | |
char | 16 bit / 2个字节 | Unicode 0 | Unicode 2^16-1 | - | Character |
📌
浮点数在机器中存放形式:浮点数 = 符号位 + 指数位 + 尾数位(尾数部分可能丢失,造成精度损失)
浮点型常量有两种表示形式:
十进制形式:如:‘3.14’,‘123.456’
科学计数法:m x 10^n 其中 m 是尾数(mantissa),n 是指数(exponent)。如:‘6.022e23’字符型存储到计算机中,需要将字符对应的码值找出来
浮点数的使用注意事项
- 精度问题:浮点数在计算机中以二进制形式表示,因此可能无法精确地表示某些十进制小数。这可能会导致精度丢失和舍入误差。因此,避免在浮点数上进行精确的比较操作,尤其是使用等于操作符(==)。
- 避免直接比较:由于精度问题,最好避免直接比较两个浮点数是否相等。而是使用范围或容差来进行比较,例如判断两个浮点数的差值是否在某个小范围内。
- 使用 BigDecimal 类处理精确计算:如果需要进行精确的小数计算,尤其是货币计算等需要高精度的场景,可以使用 BigDecimal 类来代替浮点数。
- 谨慎使用浮点数进行循环计数:在循环中对浮点数进行计数时,可能由于精度问题导致循环终止条件无法达到。这时候最好使用整数计数器,或者谨慎设计循环终止条件。
- 了解浮点数表示范围:了解浮点数的表示范围和精度,避免超出范围或精度不足的情况。
- 避免除以零:除以零会导致浮点数的特殊值,如 Infinity 或 NaN(Not a Number)。在进行除法运算时,务必确保被除数不为零。
对于数值类型的基本类型的取值范围,我们无需强制去记忆,因为它们的值都已经以常量的形式定义在对应的包装类中了。可以通过以下方式获取:
public class PrimitiveTypeTest {
public static void main(String[] args) {
// byte
System.out.println("基本类型:byte 二进制位数:" + Byte.SIZE);
System.out.println("包装类:java.lang.Byte");
System.out.println("最小值:Byte.MIN_VALUE=" + Byte.MIN_VALUE);
System.out.println("最大值:Byte.MAX_VALUE=" + Byte.MAX_VALUE);
System.out.println();
}
}
//输出结果:
/*
基本类型:byte 二进制位数:8
包装类:java.lang.Byte
最小值:Byte.MIN_VALUE=-128
最大值:Byte.MAX_VALUE=127
*/
5.2 数据类型转换
整型、实型(常量)、字符型数据可以混合运算。运算中,不同类型的数据先转化为同一类型,然后进行运算。
低 --------------------------------------------------------> 高
byte->short->char—>int—>long—>float—>double
数据类型转换注意事项:
- 不能对boolean类型进行类型转换。
- 不能把对象类型转换成不相关类的对象
- 在把容量大的类型转换为容量小的类型时必须使用强制类型转换。
- 转换过程中可能导致溢出或损失精度
- 浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入
隐式转换(自动类型转换)
必须满足转换前的数据类型的位数要低于转换后的数据类型。
例如: short数据类型的位数为16位,就可以自动转换位数为32的int类型,同样float数据类型的位数为32,可以自动转换为64位的double类型。
byte,short,char不会发生自动类型转换,且当他们三者中出现任意运算,精度会提升为int
public class AutoChange{
public static void main(String[] args){
char c1='a';//定义一个char类型
int i1 = c1;//char自动类型转换为int
System.out.println("char自动类型转换为int后的值等于"+i1);
char c2 = 'A';//定义一个char类型
int i2 = c2+1;//char 类型和 int 类型计算
System.out.println("char类型和int计算后的值等于"+i2);
}
}
//char自动类型转换为int后的值等于97
//char类型和int计算后的值等于66
显式转换(强制类型转换)
当容量大的数据类型转换为容量小的数据类型时,需要使用强制类型转换。条件是转换的数据类型必须是兼容的。
格式:(type)value type是要强制类型转换后的数据类型
public class QiangZhiZhuanHuan{
public static void main(String[] args){
int i1 = 123;
byte b = (byte)i1;//强制类型转换为byte
System.out.println("int强制类型转换为byte后的值等于"+b);
}
}
//int强制类型转换为byte后的值等于123
5.3 引用类型
- 在java里面除去基本数据类型的其它类型都是引用数据类型
- 对象、数组、接口都是引用类型
- 所有引用类型的默认值都是null
5.4 Java常量
常量在程序运行时是不能被修改的。在 Java 中使用 final 关键字来修饰常量,声明方式和变量类似:
final double PI = 3.1415927;
虽然常量名也可以用小写,但为了便于识别,通常使用大写字母表示常量。
5.5 Java转义字符
常用转移字符
- \t:一个制表符,实现对齐功能
- \n:换行符
- \:一个\
- ":一个”
- ‘:一个’
- \r:一个回车
5.6 基本数据类型和String类型的转换
-
基本数据类型转换为String类型:
- 使用String类的valueOf()方法
int num = 10; String str = String.valueOf(num);
- 使用包装类的toString()方法
int num = 10; String str = Integer.toString(num);
- 使用连接运算符(+)
int num = 10; String str = num + "";
-
String类型转换为基本数据类型:
- 使用包装类的parseXXX()方法
String str = "10"; int num = Integer.parseInt(str);
- 使用包装类的valueOf()方法
String str = "10"; int num = Integer.valueOf(str);
📌
在进行String到基本数据类型的转换时,需要确保String中包含的内容可以正确转换为对应的基本数据类型,否则会抛出NumberFormatException异常。
可以通过包装类的parseXXX()方法来转换字符串为对应的基本数据类型,其中XXX表示具体的数据类型,如int、double等。
六、Java变量类型
6.1 变量类型
在 Java 语言中,所有的变量在使用前必须声明。
int a, b, c; // 声明三个int型整数:a、 b、c
int d = 3, e = 4, f = 5; // 声明三个整数并赋予初值
byte z = 22; // 声明并初始化 z
String s = "runoob"; // 声明并初始化字符串 s
double pi = 3.14159; // 声明了双精度浮点型变量 pi
char x = 'x'; // 声明变量 x 的值是字符 'x'。
Java 语言支持的变量类型有:
-
局部变量(Local Variables):局部变量是在方法、构造函数或块内部声明的变量,它们在声明的方法、构造函数或块执行结束后被销毁,局部变量在声明时需要初始化,否则会导致编译错误。
public void exampleMethod() { int localVar = 10; // 局部变量 // ... }
-
成员(实例)变量(Instance Variables):实例变量是在类中声明,但在方法、构造函数或块之外,它们属于类的实例,每个类的实例都有自己的副本,如果不明确初始化,实例变量会被赋予默认值(数值类型为0,boolean类型为false,对象引用类型为null)。
public class ExampleClass { int instanceVar; // 实例变量 }
-
静态变量或类变量(Class Variables):类变量是在类中用 static 关键字声明的变量,它们属于类而不是实例,所有该类的实例共享同一个类变量的值,类变量在类加载时被初始化,而且只初始化一次。
public class ExampleClass { static int classVar; // 类变量 }
-
参数变量(Parameters):参数是方法或构造函数声明中的变量,用于接收调用该方法或构造函数时传递的值,参数变量的作用域只限于方法内部。
public void exampleMethod(int parameterVar) { // 参数变量 // ... }
代码示例:
public class VarTest{ //成员变量 private int instanceVar; //静态变量 private static int staticVar; public void method(int paramVar){ //局部变量 int localVar = 10; instanceVar = localVar + 5; staticVar = paramVar + 5; System.out.println("成员变量:" + instanceVar); System.out.println("静态变量:" + staticVar); System.out.println("参数变量:" + paramVar); System.out.println("局部变量:" + localVar); } public static void main(String[] args){ VarTest v = new VarTest(); v.method(20); } } //输出结果 /* 成员变量:15 静态变量:25 参数变量:20 局部变量:10 */
6.2 Java 参数变量
Java 中的参数变量是指在方法或构造函数中声明的变量,用于接收传递给方法或构造函数的值。参数变量与局部变量类似,但它们只在方法或构造函数被调用时存在,并且只能在方法或构造函数内部使用。
accessModifier returnType methodName(parameterType parameterName1, parameterType parameterName2, ...) {
// 方法体
}
在调用方法时,我们必须为参数变量传递值,这些值可以是常量、变量或表达式。
方法参数变量的值传递方式有两种:值传递和引用传递。
- 值传递:在方法调用时,传递的是实际参数的值的副本。当参数变量被赋予新的值时,只会修改副本的值,不会影响原始值。Java 中的基本数据类型都采用值传递方式传递参数变量的值。
- 引用传递:在方法调用时,传递的是实际参数的引用(即内存地址)。当参数变量被赋予新的值时,会修改原始值的内容。Java 中的对象类型采用引用传递方式传递参数变量的值。
6.3 Java 局部变量
- 局部变量声明在方法、构造方法或者语句块中。
- 局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁。
- 局部变量必须在使用前声明,并且不能被访问修饰符修饰,因为它们的作用域已经被限制在了声明它们的方法、代码块或构造函数中。
- 局部变量只在声明它的方法、构造方法或者语句块中可见,不能被其他方法或代码块访问。
- 局部变量是在栈上分配的。
- 局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。
6.4 Java 成员变量(实例变量)
- 成员变量声明在一个类中,但在方法、构造方法和语句块之外。
- 当一个对象被实例化之后,每个成员变量的值就跟着确定。
- 成员变量在对象创建的时候创建,在对象被销毁的时候销毁。
- 成员变量的值应该至少被一个方法、构造方法或者语句块引用,使得外部能够通过这些方式获取实例变量信息。
- 成员变量可以声明在使用前或者使用后。
- 访问修饰符可以修饰成员变量。
- 成员变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把成员变量设为私有。通过使用访问修饰符可以使成员变量对子类可见。
- 成员变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定;
- 成员变量可以直接通过变量名访问。但在静态方法以及其他类中,就应该使用完全限定名:ObjectReference.VariableName。
6.5 Java 类变量(静态变量)
Java 中的静态变量是指在类中定义的一个变量,它与类相关而不是与实例相关,即无论创建多少个类实例,静态变量在内存中只有一份拷贝,被所有实例共享。
静态变量在类加载时被创建,在整个程序运行期间都存在。
- 定义方式
静态变量的定义方式是在类中使用 static 关键字修饰变量,通常也称为类变量。
public class MyClass {
public static int count = 0;
// 其他成员变量和方法
}
- 访问方式
由于静态变量是与类相关的,因此可以通过类名来访问静态变量,也可以通过实例名来访问静态变量。
MyClass.count = 10; // 通过类名访问
MyClass obj = new MyClass();
obj.count = 20; // 通过实例名访问
- 生命周期
静态变量的生命周期与程序的生命周期一样长,即它们在类加载时被创建,在整个程序运行期间都存在,直到程序结束才会被销毁。因此,静态变量可以用来存储整个程序都需要使用的数据,如配置信息、全局变量等。 - 静态变量的访问修饰符
静态变量的访问修饰符可以是 public、protected、private 或者默认的访问修饰符(即不写访问修饰符)。 - 静态变量的命名规范
静态变量(也称为类变量)的命名规范通常遵循驼峰命名法,并且通常使用全大写字母,单词之间用下划线分隔,并且要用 static 关键字明确标识。- 使用驼峰命名法: 静态变量的命名应该使用驼峰命名法,即首字母小写,后续每个单词的首字母大写。例如:myStaticVariable。
- 全大写字母: 静态变量通常使用全大写字母,单词之间用下划线分隔。这被称为"大写蛇形命名法"(Upper Snake Case)。例如:MY_STATIC_VARIABLE。
- 描述性: 变量名应该是有意义的,能够清晰地表达该变量的用途。避免使用单个字符或不具有明确含义的缩写。
- 避免使用缩写: 尽量避免使用缩写,以提高代码的可读性。如果使用缩写是必要的,确保广泛理解,并在注释中进行解释。
6.6 补充内容
变量命名规则
在 Java 中,不同类型的变量(例如实例变量、局部变量、静态变量等)有一些命名规则和约定。
遵循一些基本规则,这有助于提高代码的可读性和维护性。
以下是各种变量命名规则的概述:
- 使用有意义的名字: 变量名应该具有清晰的含义,能够准确地反映变量的用途。避免使用单个字符或无意义的缩写。
- 驼峰命名法(Camel Case): 在变量名中使用驼峰命名法,即将每个单词的首字母大写,除了第一个单词外,其余单词的首字母都采用大写形式。例如:myVariableName。
- 避免关键字: 不要使用 Java 关键字(例如,class、int、boolean等)作为变量名。
- 区分大小写: Java 是大小写敏感的,因此变量名中的大小写字母被视为不同的符号。例如,myVariable 和 myvariable 是两个不同的变量。
- 不以数字开头: 变量名不能以数字开头,但可以包含数字。
- 遵循命名约定: 对于不同类型的变量(局部变量、实例变量、静态变量等),可以采用不同的命名约定,例如使用前缀或后缀来区分。
局部变量
- 使用驼峰命名法。
- 应该以小写字母开头。
- 变量名应该是描述性的,能够清晰地表示其用途。
实例变量(成员变量)
- 使用驼峰命名法。
- 应该以小写字母开头。
- 变量名应该是描述性的,能够清晰地表示其用途。
静态变量(类变量)
- 使用驼峰命名法,应该以小写字母开头。
- 通常也可以使用大写蛇形命名法,全大写字母,单词之间用下划线分隔。
- 变量名应该是描述性的,能够清晰地表示其用途。
常量
- 使用全大写字母,单词之间用下划线分隔。
- 常量通常使用
final
修饰。
参数
- 使用驼峰命名法。
- 应该以小写字母开头。
- 参数名应该是描述性的,能够清晰地表示其用途。
类名
- 使用驼峰命名法。
- 应该以大写字母开头。
- 类名应该是描述性的,能够清晰地表示其用途。
七、Java运算符
计算机的最基本用途之一就是执行数学运算,作为一门计算机语言,Java也提供了一套丰富的运算符来操纵变量。
运算符是一种特殊的符号,用来表示数据的运算、赋值和比较等。
7.1 算术运算符
表格中的实例假设整数变量A的值为10,变量B的值为20:
操作符 | 描述 | 例子 |
---|---|---|
+ | 加法 - 相加运算符两侧的值 | A + B 等于 30 |
- | 减法 - 左操作数减去右操作数 | A – B 等于 -10 |
* | 乘法 - 相乘操作符两侧的值 | A * B等于200 |
/ | 除法 - 左操作数除以右操作数 | B / A等于2 |
% | 取模(取余) - 左操作数除以右操作数的余数(B - B / A * A) | B%A等于0 |
++ | 自增: 操作数的值增加1 | B++ 或 ++B 等于 21(区别详见下文) |
– | 自减: 操作数的值减少1 | B-- 或 --B 等于 19(区别详见下文) |
📌
Java程序中 + 号的使用
1. 当左右两边都是数值型时,做加法运算
2. 当左右两边有一个为字符串时,做字符串拼接运算System.out.println(100 + 98) //198
System.out.println(“100” + 98) //10098System.out.println(100 + 3 + “hello”) //103hello
System.out.println(“hello” + 100 + 3) //hello1003
自增自减运算符
自增(++)自减(- -)运算符是一种特殊的算术运算符,在算术运算符中需要两个操作数来进行运算,而自增自减运算符是一个操作数。(- -运算符中间无空格,由于此处显示的自减运算符类似 – 这样,为了阅读更加清晰,所以本文中的- -运算符都会含有一个空格)
public class selfAddMinus{
public static void main(String[] args){
int a = 3;//定义一个变量;
int b = ++a;//自增运算
//int b = ++a; 拆分运算过程为: a=a+1=4; b=a=4, 最后结果为b=4,a=4
int c = 3;
int d = --c;//自减运算
//int d = --c; 拆分运算过程为: c=c-1=2; d=c=2, 最后结果为d=2,c=2
System.out.println("进行自增运算后的值等于"+b);
System.out.println("进行自减运算后的值等于"+d);
}
}
//进行自增运算后的值等于4
//进行自减运算后的值等于2
- 前缀自增自减法(++a,- -a): 先进行自增或者自减运算,再进行表达式运算。
- 后缀自增自减法(a++,a- -): 先进行表达式运算,再进行自增或者自减运算。
public class selfAddMinus{
public static void main(String[] args){
int a = 5;//定义一个变量;
int b = 5;
int x = 2*++a;
int y = 2*b++;
System.out.println("自增运算符前缀运算后a="+a+",x="+x);
System.out.println("自增运算符后缀运算后b="+b+",y="+y);
}
}
//自增运算符前缀运算后a=6,x=12
//自增运算符后缀运算后b=6,y=10
7.2 关系运算符
表格中的实例整数变量A的值为10,变量B的值为20:
运算符 | 描述 | 例子 |
---|---|---|
== | 检查如果两个操作数的值是否相等,如果相等则条件为真。 | (A == B)为假。 |
!= | 检查如果两个操作数的值是否相等,如果值不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是那么条件为真。 | (A> B)为假。 |
< | 检查左操作数的值是否小于右操作数的值,如果是那么条件为真。 | (A <B)为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是那么条件为真。 | (A> = B)为假。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是那么条件为真。 | (A <= B)为真。 |
instanceof | 检查是否是类的对象(面向对象章节介绍)。 | (A instanceof Integer)为真。 |
📌
一个等号(=)是赋值操作,两个等号(==)是关系运算符
7.3 位运算符
下表列出了位运算符的基本运算,假设整数变量 A 的值为 60 和变量 B 的值为 13:
操作符 | 描述 | 例子 |
---|---|---|
按位与 & | 如果相对应位都是1,则结果为1,否则为0 | (A&B),得到12,即0000 1100 |
按位或 | 如果相对应位都是 0,则结果为 0,否则为 1 | 如果相对应位都是 0,则结果为 0,否则为 1 |
按位异或 ^ | 如果相对应位值相同,则结果为0,否则为1 | (A ^ B)得到49,即 0011 0001 |
取反 〜 | 按位取反运算符翻转操作数的每一位,即0变成1,1变成0。 | (〜A)得到-61,即1100 0011 |
左移 << | 按位左移运算符。左操作数按位左移右操作数指定的位数。 | A << 2得到240,即 1111 0000 |
右移 >> | 按位右移运算符。左操作数按位右移右操作数指定的位数。 | A >> 2得到15即 1111 |
无符号右移 >>> | 按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充。 | A>>>2得到15即0000 1111 |
- 位运算符通常用于底层系统编程、优化算法、图形处理等领域。
- 位运算符的操作数必须是整数类型(byte、short、int、long)。
7.4 逻辑运算符
下表列出了逻辑运算符的基本运算,假设布尔变量A为真,变量B为假
操作符 | 描述 | 例子 |
---|---|---|
短路与 && | 当且仅当两个操作数都为真,条件才为真。 | (A && B)为假。 |
短路或 | 如果任何两个操作数任何一个为真,条件为真。 | (A || B)为真。 |
逻辑非(取反) ! | 用来反转操作数的逻辑状态。如果条件为true,则逻辑非运算符将得到false。 | !(A && B)为真。 |
逻辑与 & | 当且仅当两个操作数都为真,条件才为真。 | (A & B) 为假。 |
逻辑或 | 只有当两个操作数都为 false 时,结果才为 false | (A | B)为真。 |
逻辑异或 ^ | 当且仅当其中一个操作数为 true 时,结果为 true | (A ^ B) 为真。 |
📌
短路(Short-Circuit)是指在逻辑表达式中,当确定整个表达式的结果时,根据左操作数的值就可以确定结果的情况下,右操作数不会被计算的行为。
短路运算符和逻辑运算符的区别
&&短路与:如果第一个条件为false,则第二个条件不会判断,最终结果为false,效率高
&逻辑与:不管第一个条件是否为false,第二个条件都要判断,效率低
|| 短路或:如果第一个条件为true,则第二个条件不会判断,最终结果为true,效率高
| 逻辑或:不管第一个条件是否为true,第二个条件都要判断,效率低
7.5 赋值运算符
下面是Java语言支持的赋值运算符:
操作符 | 描述 | 例子 |
---|---|---|
= | 简单的赋值运算符,将右操作数的值赋给左侧操作数 | C = A + B将把A + B得到的值赋给C |
+ = | 加和赋值操作符,它把左操作数和右操作数相加赋值给左操作数 | C + = A等价于C = C + A |
- = | 减和赋值操作符,它把左操作数和右操作数相减赋值给左操作数 | C - = A等价于C = C - A |
* = | 乘和赋值操作符,它把左操作数和右操作数相乘赋值给左操作数 | C * = A等价于C = C * A |
/ = | 除和赋值操作符,它把左操作数和右操作数相除赋值给左操作数 | C / = A,C 与 A 同类型时等价于 C = C / A |
(%)= | 取模和赋值操作符,它把左操作数和右操作数取模后赋值给左操作数 | C%= A等价于C = C%A |
<< = | 左移位赋值运算符 | C << = 2等价于C = C << 2 |
>> = | 右移位赋值运算符 | C >> = 2等价于C = C >> 2 |
&= | 按位与赋值运算符 | C&= 2等价于C = C&2 |
^ = | 按位异或赋值操作符 | C ^ = 2等价于C = C ^ 2 |
| = | 按位或赋值操作符C | C |= 2等价于C = C |
7.6 三元运算符
条件运算符也被称为三元运算符。该运算符有3个操作数,并且需要判断布尔表达式的值。该运算符的主要是决定哪个值应该赋值给变量。
- 基本语法:
条件表达式 ? 表达式1 : 表达式2
- 运算规则:
- 如果表达式条件为true,运算后结果为表达式1
- 如果表达式条件为false,运算后结果为表达式2
7.7 Java运算符优先级
下表中具有最高优先级的运算符在的表的最上面,最低优先级的在表的底部。
类别 | 操作符 | 关联性 |
---|---|---|
后缀 | () [] . (点操作符) | 左到右 |
一元 | expr++ expr– | 从左到右 |
一元 | ++expr --expr + - ~ ! | 从右到左 |
乘性 | * / % | 左到右 |
加性 | + - | 左到右 |
移位 | >> >>> << | 左到右 |
关系 | > >= < <= | 左到右 |
相等 | == != | 左到右 |
按位与 | & | 左到右 |
按位异或 | ^ | 左到右 |
按位或 | | | 左到右 |
逻辑与 | && | 左到右 |
逻辑或 | || | 左到右 |
条件 | ?: | 从右到左 |
赋值 | = + = - = * = / =%= >> = << =&= ^ = | = | 从右到左 |
逗号 | , | 左到右 |
7.8 关于算术运算符的补充内容
// 除法 / 的使用
System.out.println(10 / 4) ; //从数学上是2.5,但由于两个都是整形,最后计算结果也是整形,在Java程序中结果为2
System.out.println(10.0 / 4) ; //计算结果会提升精度,结果为2.5
double d = 10 / 4;
System.out.println(10 / 4); //10 / 4 = 2,2再转为浮点型结果为2.0
// 取模 % 的使用
// 取模的本质:a % b = a - a / b * b
System.out.println(10 % 3) //结果为1
System.out.println(-10 % 3) //结果为-1 -10 % 3 = -10 - (-10) / 3 * 3 = -1
System.out.println(10 % -3) //结果为1 10 % -3 = 10 - 10 / (-3) * (-3) = 1
System.out.println(-10 % -3) //结果为-1 -10 % -3 = -10 - (-10) / (-3) * (-3) = -1
//++的使用
int i = 10;
i++; //自增 等价于 i = i + 1;
++i; //自增 等价于 i = i + 1;
//单独使用时,前++和后++完全相同
int j = 8;
int k = ++j; // 等价于 j = j + 1 ;k = j
int k = j++; //等价于 k = j ;j = j + 1
//作为表达式使用时
//前++:++i先自增后赋值
//后++:i++先赋值后自增
八、Java Scanner 类
Scanner API文档:https://www.runoob.com/manual/jdk11api/java.base/java/util/Scanner.html
8.1 Scanner介绍
java.util.Scanner 是 Java5 的特征,我们可以通过 Scanner 类来获取用户的输入。
-
使用步骤:
- 导入该类所在的包
- 创建该类的对象(实例化对象)
- 调用其中的方法
-
基本语法:
Scanner s = new Scanner(System.in);
-
简单示例
import java.util.Scanner; public class ScannerInput{ public static void main(String[] args){ Scanner scan = new Scanner(System.in); System.out.println("请输入姓名:"); String name = scan.next(); System.out.println("请输入年龄:"); int age = scan.nextInt(); System.out.println("请输入薪水:"); double sal = scan.nextDouble(); System.out.println("姓名:" + name + "\n性别:" + age + "\n薪水:" + sal); } }
8.2 next 方法
接下来我们演示一个最简单的数据输入,并通过 Scanner 类的 next() 与 nextLine() 方法获取输入的字符串,在读取前我们一般需要使用 hasNext 与 hasNextLine 判断是否还有输入的数据:
import java.util.Scanner;
public class ScannerDemo {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
// 从键盘接收数据
// next方式接收字符串
System.out.println("next方式接收:");
// 判断是否还有输入
if (scan.hasNext()) {
String str1 = scan.next();
System.out.println("输入的数据为:" + str1);
}
scan.close();
}
}
执行以上程序输出结果为:
📌
$ javac ScannerDemo.java
$ java ScannerDemo
next方式接收:
corgi com
输入的数据为:corgi
可以看到 com 字符串并未输出。
8.3 nextLine 方法
import java.util.Scanner;
public class ScannerDemo {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
// 从键盘接收数据
// nextLine方式接收字符串
System.out.println("nextLine方式接收:");
// 判断是否还有输入
if (scan.hasNextLine()) {
String str2 = scan.nextLine();
System.out.println("输入的数据为:" + str2);
}
scan.close();
}
}
执行以上程序输出结果为:
📌
$ javac ScannerDemo.java
$ java ScannerDemo
nextLine方式接收:
corgi com
输入的数据为:corgi com
可以看到 com 字符串输出。
next() 与 nextLine() 区别
- next():
- 一定要读取到有效字符后才可以结束输入。
- 对输入有效字符之前遇到的空白,next() 方法会自动将其去掉。
- 只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。
- next() 不能得到带有空格的字符串。
- nextLine():
- 以Enter为结束符,也就是说 nextLine()方法返回的是输入回车之前的所有字符。
- 可以获得空白。
九、Java控制结构
9.1 顺序控制
在Java中,顺序控制是最基本的控制结构之一,它指的是程序按照代码的书写顺序依次执行,从上到下逐行执行每条语句。在顺序控制中,每一条语句都按照其在程序中的位置依次执行,直到程序结束。
- 语句执行顺序
在Java程序中,代码按照书写顺序从上到下依次执行。每条语句都在前一条语句执行完毕后再执行。 - 表达式计算顺序
在Java中,表达式的计算顺序是从左到右。例如,int result = 2 + 3 * 4;
先计算乘法,然后再加法,所以结果为14。 - 方法调用顺序
如果在代码中调用了方法,那么程序会首先执行方法中的代码,然后再回到调用方法的地方继续执行。 - 代码块的执行顺序
在Java中,一对花括号 {} 中的代码被称为代码块。代码块中的语句按照书写顺序依次执行。
顺序控制是程序中最简单也是最常见的控制结构之一,它保证了程序代码按照我们期望的顺序执行,是所有程序的基础。
9.2 分支控制
-
if
基本语法:if ( 条件表达式 ) { 执行代码块;(可以有多条语句) }
📌
当条件为true时,就会执行{ }内的代码,如果为false,就不执行
如果{ }中只有一条语句,则可以不用{ } -
if…else
基本语法:if ( 条件表达式 ) { 执行代码块1;(可以有多条语句) }else { 执行代码块2; }
📌
当条件为true时,就会执行代码块1中的代码,否则执行代码块2
如果{ }中只有一条语句,则可以省略{ } -
if…else if…else
基本语法:if ( 条件表达式 ) { 执行代码块1;(可以有多条语句) }else if{ 执行代码块2; } …… else{ 执行代码块n; }
📌
当条件为true时,就会执行代码块1中的代码,否则执行代码块2
如果{ }中只有一条语句,则可以省略{ } -
嵌套分支
基本语法:if(布尔表达式 1){ //如果布尔表达式 1的值为true执行代码 if(布尔表达式 2){ //如果布尔表达式 2的值为true执行代码 } }
📌
可以像 if 语句一样嵌套 else if…else。 -
switch分支结构
switch(表达式){ case 常量1 : //语句 break; //可选 case 常量2 : //语句 break; //可选 //你可以有任意数量的case语句 default : //可选 //语句 }
📌
1. switch 语句中的变量类型可以是: byte、short、int 或者 char。从 Java SE 7 开始,switch 支持字符串 String 类型了,同时 case 标签必须为字符串常量或字面量。
2. 当遇到 break 语句时,switch 语句终止。程序跳转到 switch 语句后面的语句执行。case 语句不必须要包含 break 语句。如果没有 break 语句出现,程序会继续执行下一条 case 语句,直到出现 break 语句。
3. switch 语句可以包含一个 default 分支,该分支一般是 switch 语句的最后一个分支(可以在任何位置,但建议在最后一个)。default 在没有 case 语句的值和变量值相等的时候执行。default 分支不需要 break 语句。
9.3 循环控制
-
for循环
for(初始化; 布尔表达式; 变量迭代) { //代码语句 }
关于 for 循环有以下几点说明:
- for循环中的初始化和变量迭代可以卸载其他地方,但两边的分号不能省略
- 循环初始值可以有多条初始化语句,但要求类型一样,中间用逗号隔开,循环变量迭代也可以有多条变量迭代语句,中间用逗号隔开
初始化//如果需要循环结束后使用该变量,可以将初始化放在循环外 for(; 布尔表达式;) { //代码语句 变量迭代 } //补充 for(;;){ //表示一个无限循环 } int count = 3; for(int i = 0,j = 0; i < count; i++,j += 2){ System.out.println("i=" + i + "j=" + j); }
-
增强for循环
Java5 引入了一种主要用于数组的增强型 for 循环。
for(声明语句 : 表达式) { //代码句子 }
声明语句:声明新的局部变量,该变量的类型必须和数组元素的类型匹配。其作用域限定在循环语句块,其值与此时数组元素的值相等。
表达式:表达式是要访问的数组名,或者是返回值为数组的方法。 -
while循环
while是最基本的循环。while( 布尔表达式 ) { //循环内容 }
只要布尔表达式为 true,循环就会一直执行下去。
-
do…while循环
对于 while 语句而言,如果不满足条件,则不能进入循环。但有时候我们需要即使不满足条件,也至少执行一次。
do…while 循环和 while 循环相似,不同的是,do…while 循环至少会执行一次。do { //代码语句 }while(布尔表达式);
注意:布尔表达式在循环体的后面,所以语句块在检测布尔表达式之前已经执行了。 如果布尔表达式的值为 true,则语句块一直执行,直到布尔表达式的值为 false。
-
多重循环
多重循环是指在程序中嵌套使用多个循环结构,以便在解决问题时能够更灵活地处理不同层次的数据或逻辑关系。在Java中,多重循环通常使用嵌套的for循环、while循环或do-while循环来实现。//打印九九乘法表 for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { System.out.print(j + " * " + i + " = " + (i * j) + "\t"); } System.out.println(); }
注意:循环一般使用两层,不要超过3层
9.4 break 关键字
break 主要用在循环语句或者 switch 语句中,用来跳出整个语句块。
break 跳出最里层的循环,并且继续执行该循环后面的语句。
break 的用法很简单,就是循环结构中的一条语句:break;
示例:
public class Test {
public static void main(String[] args) {
int [] numbers = {10, 20, 30, 40, 50};
for(int x : numbers ) {
// x 等于 30 时跳出循环
if( x == 30 ) {
break;
}
System.out.print( x );
System.out.print("\n");
}
}
}
//10
//20
break说明:
break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块
public class Main {
public static void main(String[] args) {
outerloop:
for (int i = 0; i < 5; i++) {
innerloop:
for (int j = 0; j < 5; j++) {
if (i * j > 6) {
System.out.println("Breaking");
break outerloop; // 使用标签跳出外部循环
}
System.out.println(i + " " + j);
}
}
System.out.println("Done");
}
}
//有一个外部循环 outerloop 和一个内部循环 innerloop。当内部循环中的条件 i * j > 6 成立时,使用 break outerloop; 语句将跳出外部循环。
//开发过程中尽量不使用标签
//如果没有指定break,默认退出最近的循环体
9.5 continue 关键字
continue 适用于任何循环控制结构中。作用是让程序立刻跳转到下一次循环的迭代。也就是结束本次循环,继续执行下一次循环。
在 for 循环中,continue 语句使程序立即跳转到更新语句。
在 while 或者 do…while 循环中,程序立即跳转到布尔表达式的判断语句。
示例:
public class Test {
public static void main(String[] args) {
int [] numbers = {10, 20, 30, 40, 50};
for(int x : numbers ) {
if( x == 30 ) {
continue;
}
System.out.print( x );
System.out.print("\n");
}
}
}
9.6 return 关键字
当在Java中遇到 return
关键字时,它的作用是终止当前方法的执行并返回一个值(如果方法有返回类型)。
- 终止方法执行:
return 关键字用于立即结束当前方法的执行。
当执行到 return 语句时,方法将立即返回,不再执行后续代码。 - 返回值:
如果方法声明了返回类型(除了 void),则 return 后面必须跟着一个相应类型的值。
返回值的类型必须与方法声明的返回类型相匹配。 - 多重返回点:
一个方法可以有多个 return 语句,每个 return 语句都可以返回不同的值。
在方法中可以根据条件选择不同的返回值,实现多重返回点。 - 提前返回:
return 关键字可以用于提前结束方法的执行,而不必等到方法执行结束。
这种情况通常在方法中根据条件判断需要提前返回时使用。 - finally块:
如果在方法中存在 finally 块,则 return 语句执行前会先执行 finally 块中的代码,然后再返回。
如果 finally 块中也有 return 语句,则会覆盖之前的返回值。 - 在void方法中的用法:
在 void 方法中,return 语句可以单独使用,用于提前结束方法的执行,不返回任何值。
这种情况通常用于在方法的某个条件下提前退出方法。
十、Java 数组
数组可以存放多个同一类型的数据。数组也是一种数据类型,是引用类型。
10.1 声明数组变量
首先必须声明数组变量,才能在程序中使用数组。下面是声明数组变量的语法:
dataType[] arrayRefVar; // 首选的方法
或
dataType arrayRefVar[]; // 效果相同,但不是首选方法
例:
double[] myList; // 首选的方法
或
double myList[]; // 效果相同,但不是首选方法
📌
建议使用 dataType[] arrayRefVar 的声明风格声明数组变量。 dataType arrayRefVar[] 风格是来自 C/C++ 语言 ,在Java中采用是为了让 C/C++ 程序员能够快速理解java语言。
10.2 创建数组
- 动态初始化
Java语言使用new操作符来动态创建数组
dataType[] arrayRefVar;
arrayRefVar = new dataType[arraySize];
//一、使用 dataType[arraySize] 创建了一个数组。
//二、把新创建的数组的引用赋值给变量 arrayRefVar。
//数组变量的声明,和创建数组可以用一条语句完成,如下所示:
dataType[] arrayRefVar = new dataType[arraySize];
数据类型[] 数组名 = new 数据类型[大小];
- 静态初始化
还可以使用如下的方式创建数组
dataType[] arrayRefVar = {value0, value1, ..., valuek};
数组使用注意事项:
- 数组中的元素可以是任何数据类型,包括基本数据类型和引用数据类型,但是不能混用
- 数组创建后,如果没有赋值,会有默认值
- 整型(
int
、short
、byte
、long
):0 - 浮点型(
float
、double
):0.0 - 布尔型(
boolean
):false - 字符型(
char
):‘\u0000’(空字符) - 引用类型(如对象数组):null
- 整型(
- 数组的元素是通过索引访问的。数组索引从 0 开始,所以索引值从 0 到 arrayRefVar.length-1。
- 数组下表必须在指定范围内使用,否则会报:下标越界异常(ArrayIndexOutOfBoundsException)
- 数组属于引用类型,数组型数据是对象(Object)
10.3 数组赋值机制
基本数据类型赋值,赋的值就是具体的数据,且互不影响
int n1 = 10;
int n2 = n1;
n2 = 80
//此时 n1等于 10,n2为80
而数组在默认情况下是引用传递,赋的值是地址
int[] arr1 = {1,2,3};
int[] arr2 = arr1;
arr2[0] = 10;
//此时 arr1和 arr2都为{10,2,3}
数组拷贝
如果想要传递值给数组,可以使用以下方式
int[] arr1 = {10,20,30};
int[] arr2 = new int[arr1.length];
for(int i = 0; i < arr1.length; i++){
arr2[i] = arr1[i];
}
10.4 处理数组
-
数组拷贝
//数组拷贝的几种方法 //1. 使用循环遍历 int[] sourceArray = {1, 2, 3}; int[] targetArray = new int[sourceArray.length]; for (int i = 0; i < sourceArray.length; i++) { targetArray[i] = sourceArray[i]; } //2.使用 System.arraycopy() 方法 int[] sourceArray = {1, 2, 3}; int[] targetArray = new int[sourceArray.length]; System.arraycopy(sourceArray, 0, targetArray, 0, sourceArray.length); //3.使用 Arrays.copyOf() 方法 int[] sourceArray = {1, 2, 3}; int[] targetArray = Arrays.copyOf(sourceArray, sourceArray.length); //4.使用数组流(Java 8及更高版本) int[] sourceArray = {1, 2, 3}; int[] targetArray = Arrays.stream(sourceArray).toArray();
-
数组反转
//数组反转的几种方式 //1.使用循环遍历进行数组反转 public class Main { public static void main(String[] args) { int[] array = {1, 2, 3, 4, 5}; int length = array.length; for (int i = 0; i < length / 2; i++) { int temp = array[i]; array[i] = array[length - i - 1]; array[length - i - 1] = temp; } System.out.println("反转后的数组为:"); for (int num : array) { System.out.print(num + " "); } } } //2.逆序赋值法 public class Main { public static void main(String[] args) { int[] array = {1, 2, 3, 4, 5}; int length = array.length; int[] reversedArray = new int[length]; // 创建一个新的数组用于存放反转后的结果 // 逆序赋值 for (int i = 0; i < length; i++) { reversedArray[i] = array[length - i - 1]; } System.out.println("反转后的数组为:"); for (int num : reversedArray) { System.out.print(num + " "); } } } //3.使用 Collections.reverse() 方法进行数组反转(需要将数组转换为列表) import java.util.Arrays; import java.util.Collections; public class Main { public static void main(String[] args) { Integer[] array = {1, 2, 3, 4, 5}; Collections.reverse(Arrays.asList(array)); System.out.println("反转后的数组为:"); for (int num : array) { System.out.print(num + " "); } } }
-
数组扩容
//数组扩容 import java.util.Scanner; public class ArrayAdd01{ public static void main(String[] args) { int[] arr1 = {1,2,3}; Scanner scanner = new Scanner(System.in); do{ int[] arr2 = new int[arr1.length + 1]; for(int i = 0;i < arr1.length; i++){ arr2[i] = arr1[i]; } System.out.print("请输入添加的元素:"); arr2[arr2.length-1] = scanner.nextInt(); arr1 = arr2; for (int i = 0;i < arr1.length; i++) { System.out.print(arr1[i] + " "); } System.out.println(); System.out.println("添加成功,是否继续?y/n"); if(scanner.next().charAt(0) == 'n'){ break; } }while(true); } }
10.5 多维数组
多维数组可以看成是数组的数组,比如二维数组就是一个特殊的一维数组,其每一个元素都是一个一维数组
- 动态初始化
//先声明后赋值
int[][] a;
a = new int[2][3];
int[][] a = new int[2][3];
//二维数组 a 可以看成一个两行三列的数组。
//列数不确定
int[][] a = new int[3][];
for(int i = 0;i < arr.length; i++){
a[i] = new int[i+1]
for(int j = 0;j < arr[i].length; j++){
arr[i][j] = i + 1;
}
}
- 静态初始化
int[][] twoDArray = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
十一、面向对象基础
Java作为一种面向对象语言。支持以下基本概念:
多态、继承、封装、抽象、类、对象、实例、方法、重载
11.1 Java 对象和类
对象和类
-
对象:对象是类的一个实例(对象不是找个女朋友),有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
-
类:类是一个模板,它描述一类对象的行为和状态。
下图中男孩(boy)、女孩(girl)为类(class)
下图中汽车为类(class),而具体的每辆车为该汽车类的对象(object),对象包含了汽车的颜色、品牌、名称等。
软件对象也有状态和行为。软件对象的状态就是属性,行为通过方法体现。
在软件开发中,方法操作对象内部状态的改变,对象的相互调用也是通过方法来完成。
Java 中的类
通过上图创建一个简单的类来理解下 Java 中类的定义:
public class Dog {
String breed;
int size;
String colour;
int age;
void eat() {
}
void run() {
}
void sleep(){
}
void name(){
}
}
一个类可以包含以下类型变量:
- 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
- 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
- 类变量:类变量也声明在类中,方法体之外,但必须声明为 static 类型。
一个类可以拥有多个方法,在上面的例子中:eat()、run()、sleep() 和 name() 都是 Dog 类的方法。
创建对象
对象是根据类创建的。在Java中,使用关键字 new 来创建一个新的对象。创建对象需要以下三步:
- 声明:声明一个对象,包括对象名称和对象类型。
- 实例化:使用关键字 new 来创建一个对象。
- 初始化:使用 new 创建对象时,会调用构造方法初始化对象。
下面是一个创建对象的例子:
public class Puppy{
public Puppy(String name){
//这个构造器仅有一个参数:name
System.out.println("小狗的名字是 : " + name );
}
public static void main(String[] args){
// 下面的语句将创建一个Puppy对象
Puppy myPuppy = new Puppy( "tommy" );
}
}
访问实例变量和成员方法
下面的例子展示如何访问实例变量和调用成员方法:
public class Puppy{
int puppyAge;
public Puppy(String name){
// 这个构造器仅有一个参数:name
System.out.println("小狗的名字是 : " + name );
}
public void setAge( int age ){
puppyAge = age;
}
public int getAge( ){
System.out.println("小狗的年龄为 : " + puppyAge );
return puppyAge;
}
public static void main(String[] args){
/* 创建对象 */
Puppy myPuppy = new Puppy( "tommy" );
/* 通过方法来设定age */
myPuppy.setAge( 2 );
/* 调用另一个方法获取age */
myPuppy.getAge( );
/*你也可以像下面这样访问成员变量 */
System.out.println("变量值 : " + myPuppy.puppyAge );
}
}
对象和类注意事项
当在一个源文件中定义多个类,并且还有import语句和package语句时,要特别注意这些规则。
- 一个源文件中只能有一个 public 类
- 一个源文件可以有多个非 public 类
- 源文件的名称应该和 public 类的类名保持一致。例如:源文件中 public 类的类名是 Employee,那么源文件应该命名为Employee.java。
- 如果一个类定义在某个包中,那么 package 语句应该在源文件的首行。
- 如果源文件包含 import 语句,那么应该放在 package 语句和类定义之间。如果没有 package 语句,那么 import 语句应该在源文件中最前面。
- import 语句和 package 语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。
类有若干种访问级别,并且类也分不同的类型:抽象类和 final 类等。
11.2 方法重载/重写
方法重载
方法重载是指在同一个类中,允许存在多个方法名相同但参数列表不同的方法。Java中的方法重载允许我们使用相同的方法名来实现不同的功能,通过参数的类型、个数或顺序来区分这些方法。
以下是方法重载的一些重要特点和规则:
- 方法名相同: 在同一个类中,方法重载的方法名必须相同。
- 参数列表不同:方法重载的方法参数列表必须不同。参数列表可以通过参数的类型、个数或顺序来区分。如果参数列表完全相同但返回类型不同,则不构成方法重载。
- 返回类型可以相同也可以不同:方法重载可以拥有相同的返回类型,也可以有不同的返回类型。
- 访问修饰符、返回类型、异常类型不同不构成重载:方法重载仅仅与方法的参数列表有关,与方法的访问修饰符、返回类型以及方法抛出的异常类型无关。
- 方法重载的调用:当我们调用一个被重载的方法时,编译器会根据传入的参数的类型、个数或顺序来确定调用哪个重载版本。
下面是一个简单的示例来说明方法重载的概念:
public class MethodOverloadingExample {
// 方法重载1:参数为整型
public void printNumber(int num) {
System.out.println("Integer Number: " + num);
}
// 方法重载2:参数为浮点型
public void printNumber(double num) {
System.out.println("Double Number: " + num);
}
// 方法重载3:参数为字符串
public void printNumber(String str) {
System.out.println("String: " + str);
}
// 方法重载4:参数为整型和浮点型
public void printNumber(int num, double dbl) {
System.out.println("Integer Number: " + num + ", Double Number: " + dbl);
}
public static void main(String[] args) {
MethodOverloadingExample example = new MethodOverloadingExample();
example.printNumber(10);
example.printNumber(5.5);
example.printNumber("Hello");
example.printNumber(20, 3.14);
}
}
在这个示例中,我们定义了四个重载的 printNumber
方法,分别接受不同类型和个数的参数。根据参数的类型和个数,编译器将决定调用哪个重载版本的方法。
方法重写
重写相关内容见12.5小节
11.3 可变参数
可变参数是Java中的一种特性,它允许方法接受数量可变的参数。在方法声明中,使用三个连续的点号(...
)来表示可变参数。可变参数在使用时可以传递任意数量的参数,这些参数会被封装成一个数组,方法内部可以像操作数组一样使用这些参数。
以下是可变参数的一些重要特点和规则:
- 数量可变:可变参数允许方法接受任意数量的参数,包括零个参数。
- 必须是方法的最后一个参数:如果方法有多个参数,可变参数必须放在参数列表的最后。
- 可以和其他参数组合使用:可变参数可以与其他参数一起使用,但可变参数必须是最后一个参数。
- 参数类型相同:可变参数的类型必须相同,或者是参数类型的子类型。
- 参数传递方式:可变参数实际上是一个数组,所以在方法内部可以像操作数组一样来访问这些参数。
下面是一个简单的示例来说明可变参数的使用:
public class VarargsExample {
public static void printNumbers(int... numbers) {
System.out.println("Number of arguments: " + numbers.length);
for (int num : numbers) {
System.out.print(num + " ");
}
System.out.println();
}
public static void main(String[] args) {
// 调用方法时传递不同数量的参数
printNumbers(); // 输出: Number of arguments: 0
printNumbers(1); // 输出: Number of arguments: 1 1
printNumbers(1, 2); // 输出: Number of arguments: 2 1 2
printNumbers(1, 2, 3, 4); // 输出: Number of arguments: 4 1 2 3 4
}
}
在这个示例中,printNumbers()
方法接受可变参数 numbers
,在方法内部可以将这些参数视为一个整数数组。在 main()
方法中,我们可以传递不同数量的参数给 printNumbers()
方法,它都能够正确地处理这些参数。
11.4 变量作用域
Java 中的变量作用域指的是变量在代码中可见和可访问的范围。变量作用域主要受其声明位置和访问权限修饰符的影响。
以下是 Java 中变量作用域的一些重要概念:
-
局部变量(Local Variables): 局部变量声明在方法、构造函数或任意代码块(例如
for
、while
、if
等)中。它们只能在声明它们的代码块内部使用。局部变量在代码块执行结束后就会被销毁,它们的生命周期只存在于所属代码块中。public void exampleMethod() { int localVar = 10; // 局部变量 System.out.println(localVar); // 可以在此方法内访问 }
-
实例变量(Instance Variables): 实例变量声明在类中,但在方法外。它们属于类的实例,每个类的实例都有一份独立的实例变量副本。实例变量可以被类中的任意方法访问,它们的生命周期与对象的生命周期一样长。
public class ExampleClass { int instanceVar; // 实例变量 }
-
静态变量(Static Variables): 静态变量也称为类变量,它们声明为
static
关键字。静态变量属于类而不是类的实例,因此只有一份静态变量的副本,所有类的实例共享它。静态变量可以直接通过类名访问,无需创建类的实例。public class ExampleClass { static int staticVar; // 静态变量 }
-
参数变量(Parameters): 参数变量是方法或构造函数定义的变量,它们用于接收调用者传递给方法或构造函数的值。参数变量的作用域与局部变量相同,只在方法或构造函数中可见。
public void exampleMethod(int paramVar) { // 参数变量 System.out.println(paramVar); // 可以在此方法内访问 }
-
成员变量(Member Variables): 实例变量和静态变量统称为成员变量,它们都属于类的成员。成员变量的作用域取决于其修饰符,通常是整个类。
📌
属性(成员变量)和局部变量可以重名。访问时遵循就近原则。
11.5 构造方法
构造器(Constructor),也称为构造方法,是 Java 中一种特殊类型的方法,用于初始化对象。它在创建对象时被调用,负责执行对象的初始化操作,例如初始化实例变量、分配内存等。构造器的名称必须与类名相同,没有返回类型,并且不能被显式地调用,而是在使用 new
关键字创建对象时自动调用。
每个类都有构造方法。如果没有显式地为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法。
在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。
下面是一个构造方法示例:
public class Puppy{
public Puppy(){
}
public Puppy(String name){
// 这个构造器仅有一个参数:name
}
}
以下是 Java 中构造器的一些重要特点和用法:
- 与类名相同:构造器的名称必须与类名完全相同。
- 没有返回类型:构造器没有返回类型,包括
void
,因为它们的主要目的是初始化对象,而不是返回值。 - 不能被显式调用:构造器不能像普通方法一样被显式调用,而是在使用
new
关键字创建对象时自动调用。 - 默认构造器:如果类中没有显式定义构造器,编译器会自动生成一个默认构造器。默认构造器没有参数,并且执行的操作通常是将实例变量初始化为默认值。
- 多个构造器重载:可以在类中定义多个构造器,只要它们的参数列表不同,就可以构成方法重载。这样可以提供不同的初始化选项。
- 初始化对象状态:构造器通常用于初始化对象的状态,包括初始化实例变量、执行必要的计算和设置对象的初始状态等。
- 调用父类构造器:在子类的构造器中可以使用
super()
关键字调用父类的构造器,用于执行父类的初始化操作。 - 构造器的访问修饰符:构造器可以具有访问修饰符,例如
public
、protected
、private
或默认修饰符。它们的访问级别决定了构造器可以被哪些类访问。
11.6 this、super关键字
this关键字
this关键字代表当前对象的引用。
它可以用于访问当前对象的成员变量、方法,以及调用当前对象的构造方法。this
关键字主要用于解决成员变量与局部变量同名的情况,以及在构造方法中调用其他构造方法。
以下是 Java 中 this
关键字的一些重要用法和细节:
-
引用当前对象: 在任何实例方法中,
this
关键字代表当前对象的引用。可以使用this
来访问当前对象的成员变量和方法。public class MyClass { private int num; public void setNum(int num) { this.num = num; // 使用this关键字引用当前对象的num成员变量 } }
-
区分同名变量: 当成员变量与局部变量同名时,可以使用
this
关键字来区分。在这种情况下,this
表示当前对象的成员变量。public class MyClass { private int num; public void setNum(int num) { this.num = num; // 使用this关键字引用当前对象的num成员变量 } public void display(int num) { System.out.println("Local Variable: " + num); // 使用this关键字引用当前对象的num成员变量 System.out.println("Instance Variable: " + this.num); } }
-
在构造方法中调用其他构造方法: 在构造方法中,可以使用
this
关键字调用其他构造方法。这种调用必须放在构造方法的第一行,并且只能调用同一个类中的其他构造方法。}public class MyClass { private int num; public MyClass() { this(0); // 调用带参数的构造方法 } public MyClass(int num) { this.num = num; // 初始化num成员变量 } }
-
作为方法参数传递: 可以将
this
关键字作为方法参数传递给其他方法,从而在方法内部访问当前对象。public class MyClass { private int num; public MyClass() { method(this); // 将当前对象作为参数传递给方法 } public void method(MyClass obj) { System.out.println("Object's num: " + obj.num); // 在方法中访问当前对象的成员变量 } }
super关键字
super相关内容见12.4小节
十二、面向对象进阶
12.1 包(package)
包是Java中用来组织类和接口的一种机制。它将相关的类和接口组织在一起,形成一个逻辑上的单元,有助于更好地管理和组织代码。
在Java源文件的开头,使用package
关键字声明类所属的包。包的声明必须放在源文件的第一行,紧随package
关键字之后,并且只能有一个包声明。例如:
package com.example.mypackage;
- 包名通常采用逆域名(反向Internet域名)的命名规范,以确保全球唯一性。例如:
com.example.mypackage
- 包名一般全部小写,以便与类名进行区分。
- 在Java源文件中,使用
import
关键字导入其他包中的类,以便在当前类中可以直接使用这些类。例如:
import java.util.ArrayList;
import java.util.List;
包的作用:
- 命名空间管理: 包为Java类提供了命名空间,防止类名冲突。
- 代码组织: 包可以将相关的类和接口组织在一起,便于管理和维护。
- 访问控制: 包可以控制类的访问范围,提供了一定的封装性。
- 依赖管理: 包可以管理类之间的依赖关系,帮助构建模块化的应用程序。
包的访问修饰符:
- 包访问权限(默认访问修饰符):如果没有使用
public
、protected
或private
修饰符来修饰类,则该类具有包访问权限,只能在同一个包中访问。 - 包私有访问权限:使用
private
修饰符修饰的类只能在同一个包中访问。
12.2 修饰符
Java语言提供了很多修饰符,主要分为以下两类:
- 访问修饰符
- 非访问修饰符
访问修饰符
Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
-
default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
如果在类、变量、方法或构造函数的定义中没有指定任何访问修饰符,那么它们就默认具有默认访问修饰符。
默认访问修饰符的访问级别是包级别(package-level),即只能被同一包中的其他类访问。
如下例所示,变量和方法的声明可以不使用任何修饰符。
// MyClass.java class MyClass { // 默认访问修饰符 int x = 10; // 默认访问修饰符 void display() { // 默认访问修饰符 System.out.println("Value of x is: " + x); } } // MyOtherClass.java class MyOtherClass { public static void main(String[] args) { MyClass obj = new MyClass(); obj.display(); // 访问 MyClass 中的默认访问修饰符变量和方法 } }
-
private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
私有访问修饰符是最严格的访问级别,所以被声明为 private 的方法、变量和构造方法只能被所属类访问,并且类和接口不能声明为 private。
声明为私有访问类型的变量只能通过类中公共的 getter 方法被外部类访问。
Private 访问修饰符的使用主要用来隐藏类的实现细节和保护类的数据。
public class Logger { private String format; public String getFormat() { return this.format; } public void setFormat(String format) { this.format = format; } } /* Logger 类中的 format 变量为私有变量,所以其他类不能直接得到和设置该变量的值。为了使其他类能够操作该变量,定义了两个 public 方法:getFormat() (返回 format的值)和 setFormat(String)(设置 format 的值) */
-
public: 对所有类可见。使用对象:类、接口、变量、方法
被声明为 public 的类、方法、构造方法和接口能够被任何其他类访问。
如果几个相互访问的 public 类分布在不同的包中,则需要导入相应 public 类所在的包。由于类的继承性,类所有的公有方法和变量都能被其子类继承。
以下函数使用了公有访问控制:
public static void main(String[] arguments) { // ... } //Java 程序的 main() 方法必须设置成公有的,否则,Java 解释器将不能运行该类。
-
protected: 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
protected 需要从以下两个点来分析说明:
-
子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;
-
子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法。
protected 可以修饰数据成员,构造方法,方法成员,不能修饰类(内部类除外)。
接口及接口的成员变量和成员方法不能声明为 protected。
-
修饰符 | 当前类 | 同一包内 | 子孙类(同一包) | 子孙类(不同包) | 其他包 |
---|---|---|---|---|---|
public | Y | Y | Y | Y | Y |
protected | Y | Y | Y | Y/N | N |
default | Y | Y | Y | N | N |
private | Y | N | N | N | N |
非访问修饰符
非访问修饰符用于特殊情况下改变成员的默认行为,主要有以下几种:
- static: 静态修饰符,用于创建类变量和方法。被static修饰的成员属于类而不是实例,可以直接通过类名访问。
- final: final修饰符表示该成员的值无法改变,对于类、方法和变量有不同的作用:
- 对于类:表示该类无法被继承。
- 对于方法:表示该方法无法被子类重写。
- 对于变量:表示该变量的值无法被修改(对于引用类型变量,表示引用不可变,但对象的内容可以修改)。
- abstract: 抽象修饰符,用于创建抽象类和抽象方法。抽象类不能被实例化,只能用作其他类的父类,而抽象方法没有具体实现,必须在子类中重写。
- synchronized: 同步修饰符,用于实现线程同步,可以修饰方法或代码块,确保同一时刻只有一个线程执行该代码段。
- volatile: volatile修饰符用于标记变量是易变的,多线程环境下,一个线程修改了volatile修饰的变量,其他线程可以立即看到修改后的值。
- transient: transient修饰符用于告诉虚拟机,被修饰的成员变量不需要持久化保存,主要用于防止敏感信息被持久化。
12.3 封装、继承、多态
面向对象编程(Object-Oriented Programming,OOP)的三大特征是封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)
封装(Encapsulation)
封装是指将对象的状态(数据)和行为(方法)封装在一起,并对外部隐藏对象的实现细节,只提供公共的访问接口。这样做的好处包括:
- 信息隐藏: 将对象的内部细节隐藏起来,只暴露必要的接口,防止外部直接访问和修改对象的内部状态,增强了数据的安全性和可靠性。
- 隔离变化: 封装可以使对象的实现细节和外部使用独立开来,当对象的内部实现发生变化时,不会影响到外部使用该对象的代码,提高了代码的可维护性和可复用性。
- 简化编程: 封装让客户端代码不需要了解对象的具体实现细节,只需要调用公共接口就可以完成相应的操作,简化了编程和使用对象的过程。
实例:
/* 文件名: EncapTest.java */
public class EncapTest{
private String name;
private String idNum;
private int age;
public int getAge(){
return age;
}
public String getName(){
return name;
}
public String getIdNum(){
return idNum;
}
public void setAge( int newAge){
age = newAge;
}
public void setName(String newName){
name = newName;
}
public void setIdNum( String newId){
idNum = newId;
}
}
以上实例中public方法是外部类访问该类成员变量的入口。
通常情况下,这些方法被称为getter和setter方法。
因此,任何要访问类中私有成员变量的类都要通过这些getter和setter方法。
通过如下的例子说明EncapTest类的变量怎样被访问:
/* F文件名 : RunEncap.java */
public class RunEncap{
public static void main(String args[]){
EncapTest encap = new EncapTest();
encap.setName("James");
encap.setAge(20);
encap.setIdNum("12343ms");
System.out.print("Name : " + encap.getName()+
" Age : "+ encap.getAge());
}
}
继承(Inheritance)
继承是指在已有类的基础上定义新类,新类继承了已有类的属性和方法,并且可以添加新的属性和方法,从而形成类之间的父子关系。
子类继承父类的特征和行为,使得子类具有父类相同的行为。继承具有以下特点:
- 代码复用: 继承允许新类直接使用已有类的属性和方法,避免了重复编写相似的代码,提高了代码的复用性和可维护性。
- 派生性: 新类可以在已有类的基础上进行扩展和修改,形成新的类层次结构,可以根据需求灵活地进行定制和扩展。
- 多态性: 继承是多态的基础,子类可以通过继承父类的方法并重写(Override)它们来实现多态,不同子类可以有不同的行为表现。
接下来我们通过实例来说明继承。
开发动物类,其中动物分别为企鹅以及老鼠,要求如下:
- 企鹅:属性(姓名,id),方法(吃,睡,自我介绍)
- 老鼠:属性(姓名,id),方法(吃,睡,自我介绍)
//公共父类:
public class Animal {
private String name;
private int id;
public Animal(String myName, int myid) {
name = myName;
id = myid;
}
public void eat(){
System.out.println(name+"正在吃");
}
public void sleep(){
System.out.println(name+"正在睡");
}
public void introduction() {
System.out.println("大家好!我是" + id + "号" + name + ".");
}
}
这个Animal类就可以作为一个父类,然后企鹅类和老鼠类继承这个类之后,就具有父类当中的属性和方法,子类就不会存在重复的代码,维护性也提高,代码也更加简洁,提高代码的复用性(复用性主要是可以多次使用,不用再多次写同样的代码) 继承之后的代码:
//企鹅类:
public class Penguin extends Animal {
public Penguin(String myName, int myid) {
super(myName, myid);
}
}
//老鼠类:
public class Mouse extends Animal {
public Mouse(String myName, int myid) {
super(myName, myid);
}
}
需要注意的是 Java 不支持多继承,但支持多重继承。
继承的特性
- 子类拥有父类非 private 的属性、方法。
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
- Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
- 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
继承关键字
继承可以使用 extends 和 implements 这两个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字,则默认继承 Object(这个类在 java.lang 包中,所以不需要 import)祖先类。
-
extends关键字
在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。public class Animal { private String name; private int id; public Animal(String myName, int myid) { //初始化属性值 } public void eat() { //吃东西方法的具体实现 } public void sleep() { //睡觉方法的具体实现 } } public class Penguin extends Animal{ }
-
implements关键字
使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。
public interface A { public void eat(); public void sleep(); } public interface B { public void show(); } public class C implements A,B { }
super 与 this 关键字
super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。
this关键字:指向自己的引用。
class Animal {
void eat() {
System.out.println("animal : eat");
}
}
class Dog extends Animal {
void eat() {
System.out.println("dog : eat");
}
void eatTest() {
this.eat(); // this 调用自己的方法
super.eat(); // super 调用父类方法
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Animal();
a.eat();
Dog d = new Dog();
d.eatTest();
}
}
📌
super()和this()都只能放在构造器的第一行,因此这两个方法不能共同存在于一个构造器中。
使用 final 关键字声明类,就是把类定义定义为最终类,不能被继承;或者用于修饰方法,该方法不能被子类重写。
多态(Polymorphism)
- 多态的两种表现形式
多态是指同一个方法调用由于对象不同可能会产生不同的行为,即同一操作作用于不同的对象会产生不同的结果。多态有两种表现形式:
- 编译时多态(静态多态): 通过方法的重载(Overload)实现,编译器根据方法的签名选择相应的方法。
- 运行时多态(动态多态): 通过方法的重写(Override)实现,运行时根据对象的实际类型选择调用哪个方法,实现方法的多态性。
📌
编译时多态:
- 编译时多态发生在编译阶段,也就是在源代码被编译成目标代码的过程中。
- 在编译时多态中,编译器根据方法的名称和参数列表来选择要调用的方法。
- 一个常见的例子是方法重载,即在同一个类中定义多个方法,它们的方法名相同但参数列表不同。在调用这些方法时,编译器根据方法的参数类型和数量来选择适当的方法,这就是编译时多态的体现。
运行时多态:
- 运行时多态发生在程序运行时,也就是在目标代码被加载和执行的过程中。
- 在运行时多态中,方法的调用取决于实际对象的类型,而不是引用变量的类型。
- 一个常见的例子是方法重写,即子类重写了父类的方法。在调用这个方法时,实际执行的是子类中的重写版本,而不是父类中的原始版本,这就是运行时多态的体现。
关于编译时多态和运行时多态,可以参考下述案例:
JavaSE编译时多态和运行时多态 https://blog.csdn.net/m0_54068390/article/details/136991190
多态实例
class Shape {
void draw() {}
}
class Circle extends Shape {
void draw() {
System.out.println("Circle.draw()");
}
}
class Square extends Shape {
void draw() {
System.out.println("Square.draw()");
}
}
class Triangle extends Shape {
void draw() {
System.out.println("Triangle.draw()");
}
}
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。
- 多态性的向上转型(Upcasting)和向下转型(Downcasting)
- 向上转型:向上转型是指将子类对象的引用赋值给父类引用变量的过程。
- 向下转型:向下转型是指将父类对象的引用转换为子类对象的引用的过程。
📌
需要注意的是,如果尝试将父类引用变量转换为不兼容的子类类型,则会抛出ClassCastException
异常。
- 属性的值注意事项
属性没有重写之说,属性的值看编译类型
例如Base base = new Sub();
base中的属性是Base类中的属性而不是Sub类中的 - instanceof
instanceof是比较操作符,用于判断对象的运行类型是否是某类型或某类型的子类型 - 虚函数
在 Java 中,所有的非静态方法默认都是虚函数(virtual method)。虚函数是指在运行时动态绑定到对象的方法,这意味着调用哪个方法取决于实际对象的类型而不是引用变量的类型。换句话说,虚函数的调用是基于对象的实际类型而不是引用变量的类型进行的。
在面向对象编程中,通过继承和重写(Override)机制,子类可以覆盖父类的方法,从而实现多态性。当调用一个对象的方法时,如果该方法在子类中被重写了,那么实际上会调用子类中的方法,而不是父类中的方法。这种动态绑定的特性使得 Java 中的方法调用可以适应对象的实际类型,从而提高了代码的灵活性和可扩展性。
虚函数的存在是为了多态。
Java 中其实没有虚函数的概念,它的普通函数就相当于 C++ 的虚函数,动态绑定是Java的默认行为。如果 Java 中不希望某个函数具有虚函数特性,可以加上 final 关键字变成非虚函数。 - 多态的实现方式
方式一:重写 / 重载
方式二:接口
- 生活中的接口最具代表性的就是插座,例如一个三接头的插头都能接在三孔插座中,因为这个是每个国家都有各自规定的接口规则,有可能到国外就不行,那是因为国外自己定义的接口类型。
- java中的接口类似于生活中的接口,就是一些方法特征的集合,但没有方法的实现。
方式三:抽象类和抽象方法
以下是一个更为详细的多态实例的演示:
public class Test {
public static void main(String[] args) {
show(new Cat()); // 以 Cat 对象调用 show 方法
show(new Dog()); // 以 Dog 对象调用 show 方法
Animal a = new Cat(); // 向上转型
a.eat(); // 调用的是 Cat 的 eat
Cat c = (Cat)a; // 向下转型
c.work(); // 调用的是 Cat 的 work
}
public static void show(Animal a) {
a.eat();
// 类型判断
if (a instanceof Cat) { // 猫做的事情
Cat c = (Cat)a;
c.work();
} else if (a instanceof Dog) { // 狗做的事情
Dog c = (Dog)a;
c.work();
}
}
}
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void work() {
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void work() {
System.out.println("看家");
}
}
/*
输出结果:
吃鱼
抓老鼠
吃骨头
看家
吃鱼
抓老鼠
*/
12.4 super、this关键字
super关键字
super
是 Java 中的一个关键字,用于引用当前对象的父类(超类)的成员,包括方法、变量和构造器。使用 super
关键字有以下几个主要特性:
-
调用父类构造器: 在子类的构造器中,可以使用
super
关键字来调用父类的构造器。通过super()
调用父类的无参构造器,或者通过super(参数)
调用父类的有参构造器。这样可以在子类对象初始化时,先执行父类的构造器完成父类部分的初始化工作。public class Subclass extends Superclass { public Subclass() { super(); // 调用父类的无参构造器 } public Subclass(int value) { super(value); // 调用父类的有参构造器 } }
📌
//如果子类的构造器没有显式调用父类的构造器,则会默认调用父类的无参构造器(如果父类有的话)。
//如果子类的构造器显式调用了父类的构造器,则必须在子类构造器的第一行使用 super() 调用父类的构造器,且只能调用一次。 -
调用父类方法: 在子类中,可以使用
super
关键字来调用父类的方法。这在子类重写了父类的方法,但又需要在子类方法中调用父类版本的方法时很有用。public class Subclass extends Superclass { @Override void someMethod() { super.someMethod(); // 调用父类的方法 // 子类的其他逻辑 } }
📌
使用super
关键字调用父类方法时,可以在子类中覆盖(重写)父类的方法,然后在子类的方法中使用super
调用父类的方法,这样可以在子类方法中执行父类方法的逻辑。 -
访问父类成员变量:在子类中,可以使用
super
关键字来访问父类的成员变量。如果子类和父类中有同名的成员变量,可以通过super
关键字来区分。public class Subclass extends Superclass { int value; void setValue(int value) { super.value = value; // 访问父类的成员变量 } }
📌
如果子类和父类中有同名的成员变量,可以通过 super 关键字来访问父类的成员变量。
通过 super 关键字访问父类成员变量时,可以避免与子类成员变量发生命名冲突。 -
调用父类的静态成员和静态方法: 虽然在 Java 中不能通过
super
关键字访问父类的静态成员和静态方法,但是可以直接使用父类的类名来访问。public class Subclass extends Superclass { void someMethod() { // 访问父类的静态成员和静态方法 int value = Superclass.staticVariable; Superclass.staticMethod(); } }
📌
super 关键字只能在子类中使用,而不能在父类中使用。
在静态方法中不能使用 super 关键字,因为静态方法属于类而不是对象,没有 super 对象。
this关键字
this相关内容见11.6小节
super和this关键字的区别
特点 | super | this |
---|---|---|
作用对象 | 用于子类中,引用父类的成员 | 用于类内部,引用当前对象的成员 |
引用方式 | 后跟成员名,如super.method() 、super.variable | 后跟成员名或构造器名,如this.method() 、this.variable() 、this() |
调用构造器 | 用于在子类的构造器中调用父类的构造器,必须在子类构造器的第一行使用 | 用于在一个构造器中调用同一个类的其他构造器,必须在构造器的第一行使用 |
成员访问 | 如果子类和父类有同名的成员变量或方法,可以使用super 关键字访问父类的成员 | 如果方法参数名和成员变量名相同,可以使用this 关键字来引用成员变量 |
静态上下文 | 不能在静态方法中使用 | 不能在静态方法中使用来引用当前对象,但可以在非静态方法中使用 |
12.5 Override/Overload
Override(重写)
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
示例
class Animal{
public void move(){
System.out.println("动物可以移动");
}
}
class Dog extends Animal{
public void move(){
System.out.println("狗可以跑和走");
}
}
public class TestDog{
public static void main(String args[]){
Animal a = new Animal(); // Animal 对象
Animal b = new Dog(); // Dog 对象
a.move();// 执行 Animal 类的方法
b.move();//执行 Dog 类的方法
}
}
//动物可以移动
//狗可以跑和走
方法的重写规则
- 参数列表与被重写方法的参数列表必须完全相同。
- 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
- 父类的成员方法只能被它的子类重写。
- 声明为 final 的方法不能被重写。
- 声明为 static 的方法不能被重写,但是能够被再次声明。
- 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
- 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
- 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,抛出 IOException 异常或者 IOException 的子类异常。 - 构造方法不能被重写。
- 如果不能继承一个类,则不能重写该类的方法。
Overload(重载)
重载相关内容见11.2小节
Override和Overload的区别
区别点 | 重载方法 | 重写方法 |
---|---|---|
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可以修改 | 一定不能修改 |
异常 | 可以修改 | 可以减少或删除,一定不能抛出新的或者更广的异常 |
访问 | 可以修改 | 一定不能做更严格的限制(可以降低限制) |
作用范围 | 本类 | 父子类 |
方法名称 | 必须相同 | 必须相同 |
12.6 动态绑定机制
动态绑定是面向对象编程中的一个重要概念,它允许方法调用在运行时绑定到对象实际的类型,而不是在编译时确定。
在Java中,所有类都是直接或间接地继承自Object
类。当你调用一个方法时,编译器会根据引用变量的类型来确定要调用的方法。但是,如果这个方法是被继承并重写的,Java会在运行时确定实际调用的方法,而不是在编译时确定。这就是动态绑定的本质。
通过一个简单的例子来说明:
假设我们有一个Animal
类和一个Dog
类,Dog
类继承自Animal
类,并且重写了makeSound()
方法:
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
现在,假设我们有一个Animal
类型的引用变量,但实际上它指向了一个Dog
对象:
Animal myAnimal = new Dog();
当我们调用makeSound()
方法时:
myAnimal.makeSound();
编译器会看到myAnimal
是Animal
类型的引用,因此在Animal
类中查找makeSound()
方法。但是,在运行时,实际上myAnimal
指向了Dog
对象,所以调用的是Dog
类中的makeSound()
方法。这就是动态绑定的工作原理。
十三、Object类详解
Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。
Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承 Object,成为 Object 的子类。
序号 | 修饰符/类型 | 方法 | 描述 |
---|---|---|---|
1 | protected Object | clone() | 创建并返回一个对象的拷贝 |
2 | boolean | equals(Object obj) | 比较两个对象是否相等 |
3 | protected void | finalize() | 当 GC (垃圾回收器)确定不存在对该对象的有更多引用时,由对象的垃圾回收器调用此方法。 |
4 | Class<?> | getClass() | 获取对象的运行时对象的类 |
5 | int | hashCode() | 获取对象的 hash 值 |
6 | void | notify() | 唤醒在该对象上等待的某个线程 |
7 | void | notifyAll() | 唤醒在该对象上等待的所有线程 |
8 | String | toString() | 返回对象的字符串表示形式 |
9 | void | wait() | 让当前线程进入等待状态。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。 |
10 | void | wait(long timeout) | 让当前线程处于等待(阻塞)状态,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过参数设置的timeout超时时间。 |
11 | void | wait(long timeout, int nanos) | 与 wait(long timeout) 方法类似,多了一个 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。 |
13.1 equals()方法
Object equals() 方法用于比较两个对象是否相等。
equals()方法比较两个对象,是判断两个对象引用指向的是同一个对象,即它只是检查两个对象是否指向内存中的同一个地址。
equals()默认比较的是地址,但是子类中往往重写该方法,用于判断内容是否相等,比如Integer、String等类中就重写了equals方法,此时比较的不是地址而是内容。
📌
如果子类重写了 equals() 方法,就需要重写 hashCode() 方法,比如 String 类就重写了 equals() 方法,同时也重写了 hashCode() 方法。
==和equals的区别
- ==:
- ==既可以判断基本类型,也可以判断引用类型
- 如果判断基本类型,判断的是值是否相等。
- 如果判断引用类型,判断的是地址是否相等(判定是不是同一个对象)
- equals:
- equals是Object的方法,用来比较两个对象的地址值是否相等。
- 一般情况下,类会重写equals方法用来比较两个对象的内容是否相等。比如String类中的equals()是被重写了,比较的是对象的值。
13.2 hashCode()方法
hashCode()方法用于获取对象的 hash 值。此方法是为了提高哈希表(如java.util.Hashtable提供的哈希表)的性能。
关于哈希值
-
两个引用,如果指向的是同一个对象,则哈希值一定相同
-
两个引用,如果指向的是不同的对象,则哈希值是不一样的
-
哈希值主要是根据地址计算得出的
以下实例演示了 hashCode() 方法的使用:
class Test {
public static void main(String[] args) {
// Object 使用 hashCode()
Object obj1 = new Object();
System.out.println(obj1.hashCode());
Object obj2 = new Object();
System.out.println(obj2.hashCode());
Object obj3 = new Object();
System.out.println(obj3.hashCode());
}
}
13.3 toString()方法
toString() 方法用于返回对象的字符串表示形式。
默认返回格式:对象的 class 名称 + @ + hashCode 的十六进制字符串。
以下实例演示了 toString() 方法的使用
class Test {
public static void main(String[] args) {
// toString() with Object
Object obj1 = new Object();
System.out.println(obj1.toString());
Object obj2 = new Object();
System.out.println(obj2.toString());
Object obj3 = new Object();
System.out.println(obj3.toString());
}
}
//java.lang.Object@d716361
//java.lang.Object@6ff3c5b5
//java.lang.Object@3764951d
/*
输出格式说明:
java.lang.Object - 类名
@ - 符号
d716361 - 哈希值的十六进制值
*/
📌
子类往往重写toString()方法,用于返回对象的属性信息
13.4 finalize()方法
finalize() 方法用于实例被垃圾回收器回收的时触发的操作。
当 GC (垃圾回收器) 确定不存在对该对象的有更多引用时,对象的垃圾回收器就会调用这个方法。
以下实例演示了 finalize() 方法的使用:
import java.util.*;
class Test extends GregorianCalendar {
public static void main(String[] args) {
try {
// 创建 Test 对象
Test cal = new Test();
// 输出当前时间
System.out.println("" + cal.getTime());
// finalize cal
System.out.println("Finalizing...");
cal.finalize();
System.out.println("Finalized.");
} catch (Throwable ex) {
ex.printStackTrace();
}
}
}
输出结果:
Sun Oct 11 11:36:46 CST 2020
Finalizing…
Finalized.
13.5 clone()方法
用于创建并返回调用该方法的对象的一个拷贝。这种机制允许在不了解对象具体类型的情况下进行对象复制,是实现对象复制的一种方式。clone()
方法是一个受保护的方法,其基本语法如下:
protected Object clone() throws CloneNotSupportedException
使用条件
要使用 clone()
方法,一个类必须实现 java.lang.Cloneable
接口。Cloneable
接口是一个标记接口,不包含任何方法。如果一个类没有实现 Cloneable
接口而调用了 clone()
方法,将抛出 CloneNotSupportedException
。实现了 Cloneable
接口的类能告诉 Object.clone()
方法它可以合法地对该类实例进行字段到字段的复制。
深拷贝与浅拷贝
- 浅拷贝(Shallow Copy):
clone()
方法默认执行的是浅拷贝。当对象被复制时,字段本身被复制,但是如果字段是对其他对象的引用,则复制的是引用而不是引用的对象本身。因此,原始对象和克隆对象会引用同一个对象。 - 深拷贝(Deep Copy):如果需要实现对象的深拷贝,即复制对象及其引用的所有对象,需要重写
clone()
方法来实现对象中所有引用类型的成员的复制。
重写 clone() 方法
虽然 clone()
方法在 Object
类中已经实现,但通常需要在子类中重写此方法,以实现深拷贝或满足特定的复制需求。重写 clone()
方法时,首先应调用 super.clone()
来获取对象的一个浅拷贝,然后按需复制那些需要深拷贝的成员。
下面是一个简单的例子,演示如何实现 Cloneable
接口并重写 clone()
方法:
class MyClass implements Cloneable {
private int id;
private String name;
// Constructor, getters, and setters
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class TestClone {
public static void main(String[] args) {
MyClass obj1 = new MyClass();
// Initialize obj1
try {
MyClass obj2 = (MyClass) obj1.clone();
// obj2 is a clone of obj1
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
13.6 notify()方法
Java 中的 notify()
方法是一个在对象的监视器(也称为锁)上进行操作的方法,属于 Object
类的一部分。这意味着这个方法可以被类的所有实例调用。它在多线程编程中非常重要,用于线程间的通信。当多个线程在同一个对象上等待(通过调用 wait()
方法进入等待状态),notify()
方法被调用时,它会唤醒其中一个正在等待的线程。
基本工作原理
- 等待池:所有调用了该对象的
wait()
方法而进入等待状态的线程,会被放置到该对象的等待池中。 - 唤醒线程:当对象的
notify()
方法被调用时,它会从这个对象的等待池中随机选择一个线程,并提醒它重新进入锁的竞争。注意,notify()
并不立即释放锁;当前线程仍需退出同步代码块,释放锁之后,被唤醒的线程才有机会获取到锁并继续执行。 - 同步块:
notify()
方法必须在同步代码块或同步方法中被调用,这意味着它必须在拥有对象锁的上下文中使用。
使用场景
notify()
方法通常用于那些设计有等待/通知机制的场景,比如生产者-消费者问题。在这类问题中,消费者线程可能在等待队列中等待新的资源变得可用,而生产者线程在生产新资源后,会调用 notify()
来唤醒一个(如果使用 notifyAll()
则是唤醒所有)等待的消费者线程。
以下是使用 notify()
方法的简单示例
class Message {
private String content;
public synchronized void put(String content) {
this.content = content;
notify(); // 唤醒等待这个对象的锁的一个线程
}
public synchronized String take() {
while (this.content == null) {
try {
wait(); // 当前线程等待,直到其他线程调用此对象的 notify() 或 notifyAll()
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置中断状态
}
}
String content = this.content;
this.content = null;
return content;
}
}
在这个例子中,有一个简单的 Message
类,包含用于存放消息内容的字符串。put
方法在设置消息内容后会调用 notify()
方法,这将唤醒可能正在等待消息变为非空的线程。take
方法则会在内容为空时调用 wait()
,导致调用它的线程等待直到 put
方法产生新的内容并调用 notify()
。
注意事项
- 在调用
wait()
、notify()
或notifyAll()
时,必须持有相关对象的锁。 notify()
方法只能唤醒一个等待线程,如果有多个线程等待,则选择哪个线程被唤醒是不确定的。如果你想唤醒所有等待的线程,应该使用notifyAll()
方法。- 调用
notify()
后,当前线程不会立即释放锁。锁的释放会在当前同步块执行完毕后自动进行。 - 使用
wait()
和notify()
时应尽量避免竞态条件和死锁。正确使用同步机制和对象的内部条件(状态)来控制线程间的协作是非常重要的。
13.7 notifyAll()方法
Java 中的 notifyAll()
方法是 Object
类的一个成员方法,用于在多线程环境下管理线程间的协作。当一个对象调用 notifyAll()
方法时,它会唤醒在该对象的监视器(锁)上通过调用 wait()
方法进入等待状态的所有线程。这些线程将被移到锁的等待队列中,等待当前拥有锁的线程释放锁,以便它们可以通过锁的竞争重新获得锁并继续执行。
工作原理
- 等待池和锁: Java 中的每个对象都有一个锁(监视器)和一个等待池。等待池中是调用了该对象的
wait()
方法而进入等待状态的所有线程。 - 唤醒所有线程: 当一个线程调用对象的
notifyAll()
方法时,该对象等待池中的所有线程都会被唤醒。这意味着,如果有多个线程因调用wait()
方法而处于等待状态,notifyAll()
将使它们全部唤醒并开始竞争对象的锁。 - 锁的释放: 唤醒线程并不意味着立即获得执行。唤醒的线程必须等待调用
notifyAll()
的线程释放锁后,才能通过正常的锁竞争机制尝试获取锁。因此,调用notifyAll()
的线程会继续执行直到达到同步代码块的末尾或显式释放锁。
使用场景
notifyAll()
方法通常用在需要通知所有等待线程的情况下,比如在使用条件变量进行线程间通信时。如果一个条件现在对多个等待线程都成立,使用 notifyAll()
可以确保所有相关线程都能被唤醒并重新检查条件。这对于某些算法或设计模式(如生产者-消费者模式)中,多个线程可能在等待同一个条件变为真的情况非常有用。
以下是一个简单的使用 notifyAll()
方法的示例:
class SharedResource {
private boolean isReady = false;
public synchronized void makeReady() {
isReady = true;
notifyAll(); // 唤醒所有在此对象锁上等待的线程
}
public synchronized void waitForReady() {
while (!isReady) {
try {
wait(); // 等待直到其他线程调用 notifyAll()/notify()
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 设置中断状态
}
}
// 执行一些操作
}
}
在这个例子中,SharedResource
类中有一个表示资源是否准备就绪的标志 isReady
。makeReady()
方法将此标志设置为 true
并调用 notifyAll()
方法,以唤醒所有可能正在 waitForReady()
方法中等待 isReady
变为 true
的线程。被唤醒的线程将会重新检查 isReady
条件,如果条件满足,则继续执行。
注意事项
notifyAll()
方法必须在同步代码块或方法中调用,确保调用它的线程持有对象的锁。- 使用
notifyAll()
而不是notify()
可以避免某些类型的死锁,尤其是在多个线程等待不同条件变为真的情况下。 - 在唤醒所有等待的线程后,每个线程会尝试重新获得锁。只有一个线程能成功获取锁并继续执行;其余线程将继续在锁的等待队列中等待。
13.8 wait()方法
Java中的wait()
方法是Object
类的一个重要方法,它在多线程编程中用于线程间的通信。该方法用于使当前线程等待,直到另一个线程调用同一个对象上的notify()
或notifyAll()
方法,或者直到超过指定的时间量。在调用wait()
方法时,当前线程必须持有该对象的锁。调用wait()
方法后,当前线程会释放该对象上的锁,并进入该对象的等待池中,直到被通知(notify)或被中断。
基本工作原理
- 锁的释放:当线程执行到
wait()
方法时,它会释放持有的对象锁,这使得其他线程可以进入同步代码块或同步方法,执行操作并有机会通知(waiting)线程。 - 等待通知:调用
wait()
方法的线程会进入对象的等待池并等待通知。如果调用wait()
时不带参数,线程会无限期等待,直到被notify()
或notifyAll()
方法唤醒。也可以通过wait(long timeout)
和wait(long timeout, int nanos)
提供超时时间,让线程等待指定的时间长度。 - 重新获取锁:当线程被
notify()
方法唤醒或wait()
方法的等待时间结束时,线程会从等待池移动到锁的等待队列。它必须重新竞争并获得对象的锁,才能继续执行。
使用场景
wait()
方法通常用于生产者-消费者问题中,或者当线程需要等待特定条件满足才能继续执行时。例如,在一个线程需要等待另一个线程完成某项操作(如设置了某个条件或变量)之后才能继续执行的场景中,就可以使用wait()
和notify()
或notifyAll()
来实现这种同步。
以下是使用 notify()
方法的简单示例:
class SharedObject {
private boolean condition = false;
public synchronized void waitForCondition() throws InterruptedException {
while (!condition) {
wait(); // 线程在这里等待condition变为true
}
// 条件满足后,继续执行
}
public synchronized void setCondition(boolean newCondition) {
condition = newCondition;
notifyAll(); // 通知所有等待的线程
}
}
13.9 wait(long timeout)方法
wait(long timeout)
方法是 Java 中 Object
类的一个重载版本的 wait()
方法,它允许线程在指定的时间内等待,直到其他线程调用此对象的 notify()
方法或 notifyAll()
方法为止。如果指定的等待时间过去之前被通知,或者如果线程在等待时被中断,线程将提前退出等待状态。这个方法提供了一种方式,让线程等待某个条件在指定的时间内满足,而不是无限期地等待。
参数说明
- timeout:参数类型为
long
,单位是毫秒(ms)。它指定当前线程在等待通知之前应该等待的最长时间。如果值为0
,则表示线程将无限期等待,直到被通知。
工作原理
当线程调用对象的 wait(long timeout)
方法时,会发生以下几件事情:
- 锁的释放:当前线程会释放对该对象的锁,使得其他线程可以进入该对象的同步代码块或同步方法。
- 进入等待状态:当前线程进入对象的等待池中,等待其他线程的通知或中断,或者直到超过指定的等待时间。
- 等待时间到期或接收通知:如果在指定的等待时间内,其他线程调用了相同对象的
notify()
或notifyAll()
方法,或者当前线程被中断,则当前线程会尝试重新获取对象的锁。如果在等待时间结束之前没有被通知或中断,线程也会尝试重新获取锁。 - 重新竞争锁:一旦线程被唤醒(无论是由于通知、中断还是等待时间到期),它就会进入该对象锁的竞争中。只有当线程获取到了锁之后,才能继续执行。
使用场景
wait(long timeout)
方法通常用在需要线程等待某个条件满足,但又不希望线程无限期等待的场景。例如,在实现一些具有超时需求的同步操作时,这个方法非常有用。
下面是一个使用 wait(long timeout)
方法的示例:
class TimeoutWaitExample {
private static final Object lock = new Object();
public void performAction() throws InterruptedException {
synchronized (lock) {
System.out.println("Action started, waiting for condition...");
lock.wait(5000); // 等待最多5秒
System.out.println("Action resumed or timeout reached.");
}
}
public static void main(String[] args) throws InterruptedException {
TimeoutWaitExample example = new TimeoutWaitExample();
example.performAction();
}
}
注意事项
- 在调用
wait(long timeout)
方法时,必须确保当前线程持有对象的锁。 - 调用
wait(long timeout)
方法后,线程会释放锁,并进入等待状态。 - 等待期间,线程可以通过
notify()
,notifyAll()
方法的调用,或者被中断来提前唤醒。 - 如果指定的等待时间到期,线程也会被唤醒,然后尝试重新获取锁。
- 推荐在循环中调用
wait()
方法来检查条件,以避免虚假唤醒和确保条件确实满足。
13.10 wait(long timeout, int nanos)方法
wait(long timeout, int nanos)
方法是 Java 中 Object
类的一个重载版本,它提供了一种在更细粒度的时间内等待对象的通知的能力。这个方法允许线程等待直到其他线程调用此对象的 notify()
或 notifyAll()
方法,或者直到某个指定的时间量过去,以较小的时间单位进行计算,包括毫秒和纳秒。
参数说明
- timeout:这是主要的等待时间,以毫秒为单位。这部分指定了等待通知的大致时间长度。
- nanos:这是额外的时间,以纳秒为单位,用来指定更精确的等待时间。这个值必须在0到999999之间(包括0和999999)。
工作原理
当一个线程调用一个对象的 wait(long timeout, int nanos)
方法时,线程会做以下事情:
- 释放锁:当前线程会释放对该对象的锁,允许其他线程访问同步代码块或方法。
- 进入等待状态:线程随后进入该对象的等待池,等待其他线程的
notify()
或notifyAll()
调用,或是等待指定的时间量过去。 - 被唤醒或时间到期:如果在指定的时间内,其他线程调用了该对象的
notify()
或notifyAll()
方法,或者线程被中断,或者指定的等待时间结束,则线程会尝试重新获取该对象的锁。 - 锁的竞争:一旦被唤醒,线程将进入该对象锁的竞争队列,等待获取锁。只有当线程获得锁后,才能继续执行。
使用场景
这个方法通常用于需要精确控制等待时间的情况,特别是当等待时间需要精确到纳秒级别时。然而,大多数操作系统不支持纳秒级别的时间等待精度,所以实际的等待时间可能不会那么精确,依赖于操作系统的时间调度精度。
下面是使用 wait(long timeout, int nanos)
方法的一个简单示例:
public class WaitExample {
public static void main(String[] args) {
final Object lock = new Object();
Thread thread = new Thread(() -> {
synchronized (lock) {
try {
// 等待1秒加500万纳秒(即总共1.005秒)
lock.wait(1000, 500000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 设置中断标志
}
System.out.println("Thread awakened");
}
});
synchronized (lock) {
try {
System.out.println("Starting thread and waiting for it to finish");
thread.start();
// 主线程等待一段时间后唤醒等待线程
Thread.sleep(1500); // 确保等待线程进入等待状态
lock.notifyAll(); // 唤醒等待线程
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 设置中断标志
}
}
}
}
注意事项
wait(long timeout, int nanos)
必须在同步代码块或同步方法中调用,因为它需要当前线程持有对象的锁。- 调用此方法会导致当前线程释放锁,进入等待状态。
- 类似于
wait()
和wait(long timeout)
方法,等待时间的精确性取决于系统定时器和调度器的精度和效率。 - 使用时应注意
InterruptedException
的正确处理,以及在循环中检查等待条件,避免虚假唤醒问题。
13.11 getClass()方法
getClass() 方法用于获取对象的运行时对象的类。
返回值Class
对象,它是一个类的元数据对象,包含了关于类的各种信息,如类名、包名、父类、实现的接口等。
示例
public class Main {
public static void main(String[] args) {
String str = "Hello";
Class<?> cls = str.getClass();
System.out.println("Class of str: " + cls.getName());
Integer num = 10;
cls = num.getClass();
System.out.println("Class of num: " + cls.getName());
}
}
📌
getClass方法通常用于以下情况
运行时类型检查:通过getClass()
方法可以在运行时获取对象的实际类型,从而进行类型检查或处理。
反射:getClass()
方法是反射机制的基础之一,可以通过Class
对象获取类的各种信息,并进行动态操作,如创建对象、调用方法等。
对象工厂:在设计对象工厂模式时,可以通过getClass()
方法获取对象的类信息,从而根据不同的类名创建相应的对象。对于数组对象,
getClass()
方法返回的是数组的运行时类,而不是数组元素的类。
对于基本数据类型的包装类,getClass()
方法返回的是包装类的运行时类,而不是对应的基本数据类型的类。
十四、面向对象高级
14.1 static关键字
在Java中,static
关键字用于创建静态变量、静态方法和静态代码块。
- 静态变量(Static Variables)
静态变量也称为类变量,它们属于类而不是类的任何单个实例。无论创建了多少个类的实例,静态变量只会有一份拷贝。它们在类加载时被初始化,而不是在创建实例时。可以直接通过类名访问静态变量,而无需实例化类。
public class MyClass {
static int count = 0; // 静态变量
public MyClass() {
count++; // 每次创建实例时增加计数器
}
}
- 静态方法(Static Methods)
静态方法是属于类的方法,而不是特定的实例。它们可以直接通过类名调用,无需实例化类。静态方法通常用于执行与类相关的任务,而不依赖于实例的特定状态。
public class MyClass {
public static void staticMethod() {
System.out.println("This is a static method");
}
}
📌
普通成员方法,既可以访问静态成员,也可以访问非静态成员
而静态方法(类方法)只能访问静态成员和静态方法
- 静态代码块(Static Blocks)
静态代码块用于在类加载时执行一次性初始化操作。它们在类加载时首先执行,并且只执行一次。通常用于初始化静态变量或执行其他类级别的初始化任务。
public class MyClass {
static {
System.out.println("Static block initialized.");
}
}
static
关键字有一些使用细节和限制,这些细节需要注意:
- 静态变量初始化:
静态变量在类加载时被初始化,并且只初始化一次。它们的初始化可以在声明时进行,或者在静态代码块中进行。 - 静态方法调用:
静态方法可以直接通过类名调用,无需创建类的实例。静态方法不能直接访问非静态成员,因为它们不依赖于实例状态。 - 静态成员的访问权限
public
、protected
、private
:
静态成员可以有不同的访问权限,可以是public
、protected
、private
或默认(不写访问修饰符)。但是,静态成员只能访问相同类中的其他静态成员或静态方法,不能直接访问实例变量或实例方法。 - 静态代码块的执行顺序:
如果有多个静态代码块,在类加载时它们按照在类中出现的顺序依次执行。静态代码块只会执行一次,即在类加载时执行。 - 静态变量的生命周期:
静态变量的生命周期与类的生命周期相同,它们在类被加载时初始化,在类被卸载时销毁。因此,它们的生命周期可能会比实例变量长。 - 静态变量的共享性:
静态变量是类级别的,它们在内存中只有一份拷贝,被所有类的实例共享。因此,如果一个实例修改了静态变量的值,那么这个值对于所有其他实例也是可见的。 - 静态成员在继承中的行为:
如果子类中声明了与父类中相同的静态成员(变量或方法),则子类中的静态成员会隐藏父类中的对应成员,而不是覆盖。但是,父类和子类的静态成员仍然相互独立,它们不会相互影响。 - 静态内部类
内部类可以被声明为静态,这意味着它们不依赖于外部类的实例而存在。静态内部类可以直接通过外部类名访问,并且它们不能访问外部类的非静态成员。
14.2 main()方法详解
在Java中,main
方法是程序的入口点(entry point),是每个独立程序的起始点。当你运行一个Java应用程序时,Java虚拟机(JVM)会自动寻找并执行main
方法。
- 方法签名
main
方法的声明必须是public static void
,并且方法名必须是main
,参数列表为一个String
数组(或称为命令行参数)。
public static void main(String[] args) {
// main方法的代码
}
- 入口点
Java应用程序的执行从main
方法开始。当你运行一个Java程序时,JVM会首先加载并执行类中的main
方法。 - 静态方法:
main
方法必须声明为static
,因为在执行main
方法时,类还没有实例化,因此不能通过实例调用方法。
通过将main
方法声明为静态,可以在不创建类的实例的情况下调用它。 - 参数
main
方法接受一个String
类型的数组作为参数,通常用于接收命令行输入的参数。这个参数数组包含了运行Java程序时传递的命令行参数。
args[0]
表示第一个命令行参数args[1]
表示第二个命令行参数- 以此类推
- 方法体
main
方法的方法体中包含了程序的实际逻辑。在main
方法中,你可以编写程序的初始化、控制流程、调用其他方法等等。 - 返回类型
main
方法没有返回值,其返回类型是void
,因为Java应用程序的执行在main
方法结束时就结束了。 - 异常处理
由于main
方法是程序的入口点,因此应该在main
方法中处理可能抛出的异常。通常使用try-catch
块来捕获异常并进行处理,或者使用throws
关键字声明main
方法可能抛出的异常。
14.3 final修饰符
final 表示"最后的、最终的"含义,变量一旦赋值后,不能被重新赋值。被 final 修饰的实例变量必须显式指定初始值。
final的使用场景:
- 当不希望类被继承时,可以用final修饰
- 当不希望父类的某个方法被子类覆盖/重写时,可以用final关键字修饰
- 当不希望类的某个属性的值被修改,可以用final修饰
- 当不希望某个局部变量被修改,可以使用final修饰
final使用注意事项: - final修饰的属性又叫常量,一般用XX_XX命名(大蛇型命名法)
- final修饰的属性在定义时,必须赋初值,且以后不能再修改,赋值可以在定义时,在构造器中,在代码块中
- 如果final修饰的属性时静态的,初始化的位置只能是在定义时,在静态代码块中,不能在构造器中赋值
- final类不能继承,但是可以实例化对象
- 如果类不是final类,但是含有final方法,则该方法虽然不能重写,但是可以被继承
- 一般来说,如果一个类已经是final类了,就没有必要再将方法修饰成final方法了
- final不能修饰构造方法
- final 修饰符通常和 static 修饰符一起使用来创建类常量,不会导致类的加载,效率更高。
- 包装类(Integer,Double,float,Boolean)等都是final类,String类也是final类
14.4 抽象类
当父类中的某些方法,需要声明,但是又不确定如何实现时,可以将其声明为抽象方法,这个类就是抽象类
抽象类(Abstract Class)是Java中一种特殊的类,它不能被实例化,只能被用作其他类的父类。抽象类通常用于定义一组相关的类的通用结构和行为,并且可以包含抽象方法和具体方法。
定义抽象类
public abstract class Animal {
// 抽象方法
public abstract void makeSound();
// 具体方法
public void eat() {
System.out.println("Animal is eating");
}
}
实例化抽象类
由于抽象类不能被实例化,因此无法使用new
关键字来创建抽象类的对象。但是,可以通过子类来实例化抽象类。例如:
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.makeSound(); // Output: Woof
dog.eat(); // Output: Animal is eating
}
}
抽象类特点
- 抽象类不能被实例化,只有抽象类的非抽象子类可以创建对象。
- 抽象类不一定要包含抽象方法,但是有抽象方法的类必定是抽象类。
- 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
- 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
- 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。
- 抽象方法不能使用private,final,static来修饰,因为这些关键字和重写相违背。
14.5 接口
接口(Interface)是一种抽象类型,用于定义一组方法的规范,而不包含方法的具体实现。接口可以被类实现,从而使得类能够遵循接口定义的方法规范。接口在Java中起到了一种重要的角色,用于实现多态性、组件之间的松耦合等。
定义接口
public interface MyInterface {
// 声明方法,不包含方法体
void method1();
int method2();
}
📌
在接口中,只能声明方法,不能包含字段、静态方法以及具体实现。接口中的方法默认为抽象方法,可以包含默认方法(使用default
关键字修饰)和静态方法(使用static
关键字修饰)。
实现接口
实现接口的类必须提供接口中声明的所有方法的具体实现。
public class MyClass implements MyInterface {
@Override
public void method1() {
// 实现方法1的具体逻辑
}
@Override
public int method2() {
// 实现方法2的具体逻辑
return 0;
}
}
📌
在jdk1.7之前,接口里的所有方法都没有方法体,即都是抽象方法,
在jdk1.8之后,接口中可以有静态方法,默认方法,也就是可以有方法的具体实现。
public interface MyInterface {
void method1(); // 抽象方法
default void method2() {
// 默认实现的方法
}
}
public interface MyInterface {
static void staticMethod() {
// 静态方法的实现
}
}
接口的特性
- 多继承:一个类可以实现多个接口,从而拥有多个接口的方法。
- 实现替换:一个类可以在运行时替换实现的接口,从而实现不同的行为。
- 扩展性:接口的设计允许系统在不修改原有代码的情况下进行功能扩展。
- 代码复用:接口提供了一种标准化的方法,可以被多个类实现,从而实现代码的复用。
接口其他特性
-
接口不能被实例化
-
接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
-
接口中的所有方法是public方法,接口中的抽象方法可以不用abstract修饰
-
接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。访问时使用接口名.属性名的方式
-
一个普通类实现接口,必须将该接口的所有方法都实现
-
接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
-
一个类可以实现多个接口
-
接口不能继承其它类,但可以继承多个其他接口
//此时 B,C皆为 interface interface A extends B,C
接口和抽象类对比
特性 | 接口 | 抽象类 |
---|---|---|
定义关键字 | interface | abstract class |
继承 | 接口不能继承其他类或接口 | 抽象类可以继承其他类或抽象类 |
多继承 | 一个类可以实现多个接口 | 一个类只能直接继承一个类 |
构造函数 | 接口不能有构造函数 | 抽象类可以有构造函数 |
字段 | 接口不能包含字段 | 抽象类可以包含字段 |
方法 | 接口只能包含抽象方法 | 抽象类可以包含抽象方法和具体方法 |
默认方法 | 接口可以包含默认方法(Java 8+) | 抽象类不能包含默认方法 |
静态方法 | 接口可以包含静态方法(Java 8+) | 抽象类不能包含静态方法 |
实现原理 | 类实现接口的所有方法 | 子类必须实现抽象类的所有抽象方法 |
使用场景 | 适用于描述一组类的通用行为和功能 | 适用于描述类之间的层次关系,提供通用的部分,如模板方法等 |
灵活性 | 接口提供了更大的灵活性,一个类可以实现多个接口 | 抽象类提供了一种层次关系,子类可以继承一个抽象类,并且可以添加额外的功能 |
可见性 | 接口的方法默认为public,字段默认为public static final | 抽象类的成员变量和方法的可见性可以是任意的(public、protected、private等) |
14.6 内部类
在java中,可以将一个类定义在另一个类里面或一个方法里面,这样的类称为内部类。它可以访问外部类的成员变量和方法,并且可以与外部类的实例产生关联。
内部类的类型一般包含以下四种:
- 成员内部类
- 静态内部类
- 局部内部类
- 匿名内部类
成员内部类(Member Inner Class)
成员内部类是定义在另一个类的内部的普通类,它可以直接访问外部类的所有成员变量和方法,包括私有成员。成员内部类可以使用外部类的实例来创建对象,也可以在外部类的静态方法中创建对象。
public class OuterClass {
private int outerVar;
// 成员内部类
public class InnerClass {
public void innerMethod() {
// 访问外部类的成员变量
outerVar = 10;
// 访问外部类的方法
outerMethod();
}
}
public void outerMethod() {
System.out.println("Outer method");
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.innerMethod();
}
}
静态内部类(Static Nested Class)
静态内部类是定义在另一个类内部但使用 static
关键字修饰的类,它与外部类的实例无关,可以直接通过外部类名访问。静态内部类不能访问外部类的非静态成员,但可以访问外部类的静态成员。
public class OuterClass {
private static int outerVar;
// 静态内部类
public static class StaticInnerClass {
public void innerMethod() {
// 访问外部类的静态成员变量
outerVar = 10;
// 访问外部类的静态方法
outerMethod();
}
}
public static void outerMethod() {
System.out.println("Outer method");
}
public static void main(String[] args) {
OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass();
inner.innerMethod();
}
}
局部内部类(Local Inner Class)
局部内部类是定义在方法内部的类,它只能在定义它的方法内部访问,作用域被限制在方法内部。局部内部类可以访问外部类的成员变量和方法,但访问时要求外部类的变量是final
的(Java 8之后,可以不声明为final
,但其实质是隐式final)
局部内部类不能添加访问修饰符,因为它相当于一个局部变量,局部变量时不能使用修饰符的,但是可以使用final
修饰。
public class OuterClass {
private int outerVar = 10;
public void outerMethod() {
final int localVar = 20;
// 局部内部类
class LocalInnerClass {
public void innerMethod() {
// 访问外部类的成员变量
System.out.println(outerVar);
// 访问局部变量
System.out.println(localVar);
}
}
LocalInnerClass inner = new LocalInnerClass();
inner.innerMethod();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.outerMethod();
}
}
📌
如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,可以使用外部类名.this.成员。
匿名内部类(Anonymous Inner Class)
匿名内部类是没有类名的内部类,通常是在创建对象的同时定义类,并且通常继承一个类或实现一个接口。匿名内部类可以直接在方法参数中创建,或者作为方法的返回值。
public class OuterClass {
interface MyInterface {
void method();
}
public MyInterface createAnonymousClass() {
return new MyInterface() {
@Override
public void method() {
System.out.println("Anonymous class method");
}
};
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
MyInterface anonymous = outer.createAnonymousClass();
anonymous.method();
}
}
匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。
14.7 枚举
Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一年的 12 个月份,一个星期的 7 天,方向有东南西北等。
Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割。
enum Color
{
RED, GREEN, BLUE;
}
public class Test
{
// 执行输出结果
public static void main(String[] args)
{
Color c1 = Color.RED;
System.out.println(c1);
}
}
📌
如果使用enum实现枚举,必须将定义常量对象写在前面
使用enum关键字定义一个枚举类时,默认会继承Enum类(当使用enum后,就不能继承其他类了,但是可以实现接口)
Enum 枚举类
下面是关于Java中Enum类中常用方法的讲解:
方法 | 描述 |
---|---|
values() | 返回枚举类中定义的所有枚举常量组成的数组。 |
valueOf(String name) | 返回具有指定名称的枚举常量。如果不存在具有指定名称的枚举常量,则抛出 IllegalArgumentException 异常。 |
name() | 返回枚举常量的名称,与声明时的名称一致。 |
ordinal() | 返回枚举常量在枚举声明中的位置索引,从0开始计数。 |
compareTo(Enum<?> o) | 比较两个枚举常量的顺序,返回值为负数表示当前枚举常量位于参数枚举常量之前,0表示相等,正数表示位于之后。 |
toString() | 返回枚举常量的字符串表示,等同于调用 name() 方法。 |
equals(Object other) | 比较枚举常量是否相等,当且仅当参数是同一个枚举常量时返回 true。 |
getDeclaringClass() | 返回枚举常量所属的枚举类的 Class 对象。 |
hashCode() | 返回枚举常量的哈希码值,与枚举常量的名称的哈希码值相同。 |
14.8 注解
Java的注解(Annotation)是一种用于为程序元素(类、方法、字段等)添加元数据的特殊标记。注解本身不会影响程序的运行,但可以在编译时、类加载时或运行时被其他程序(如编译器、框架、工具等)读取和处理,从而实现一些特定的功能。
注解的定义
在Java中,注解使用 @
符号作为前缀,紧跟着注解的名称和一对圆括号,括号内可以包含一些参数。注解的定义可以是预定义的,也可以是自定义的。
预定义注解:
@Override
: 表示当前方法覆盖了父类的方法。@Deprecated
: 表示当前元素已被弃用。@SuppressWarnings
: 告诉编译器忽略特定类型的警告。@FunctionalInterface
: 表示当前接口是一个函数式接口。- 等等
自定义注解使用 @interface
关键字定义,可以在注解中定义一些元素(成员变量),这些元素可以是基本类型、枚举类型、类类型,也可以是数组类型。例如:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
String value() default "default value";
int num() default 0;
}
元注解
元注解是用于注解其他注解的注解,常用的元注解有四种:
@Retention
: 用于指定注解的生命周期,即注解在什么时候生效。可选的值有SOURCE
(源码级别)、CLASS
(类文件级别)和RUNTIME
(运行时级别)。@Target
: 用于指定注解可以应用的目标类型,如类、方法、字段等。@Documented
: 表示注解应该包含在JavaDoc文档中。@Inherited
: 表示注解可以被子类继承。
使用注解
注解可以应用于类、方法、字段、参数等程序元素上。使用注解时,只需在目标元素前面添加注解即可,可以使用参数为注解传递信息。
@MyAnnotation(value = "test", num = 10)
public void myMethod() {
// 方法体
}
注解在程序运行时可以被反射机制读取和处理。常见的处理方式包括:
- 编译时处理:在编译时,通过注解处理器处理注解,生成额外的代码或配置文件。
- 类加载时处理:在类加载时,通过自定义类加载器读取注解信息。
- 运行时处理:在程序运行时,通过反射机制读取和处理注解,实现一些特定的功能。
十五、Java异常处理
15.1 异常介绍
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
Java中的异常是指在程序运行过程中发生的意外情况或错误,它们可以影响程序的正常执行流程。
在Java中,异常处理可以通过以下方式实现:
try-catch
语句:用于捕获并处理异常,catch
块中可以针对不同的异常类型进行不同的处理。throws
关键字:用于声明方法可能抛出的异常,将异常传递给调用者处理。finally
块:用于定义在try
块中的代码执行完毕后必须执行的清理代码,无论是否发生异常。
15.2 异常体系
Java中的异常类都是继承自 Throwable
类,它有两个主要的子类:
Exception
:通常表示程序中可能会遇到的问题,应该捕获和处理。Error
:通常表示严重的系统问题或虚拟机问题,通常无法恢复,程序不应该捕获并处理。
📌
Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。
Error 用来指示运行时环境发生的错误。
例如,JVM 内存溢出。一般地,程序不会从错误中恢复。
编译时异常和运行时异常
- 运行时异常
都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。 - 非运行时异常 (编译异常)
是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
受检异常(Checked Exception)和非受检异常(Unchecked Exception) - 受检异常(Checked Exception)
正确的程序在运行中,很容易出现的、情理可容的异常状况。受检异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。 - 非受检异常(Unchecked Exception)
包括运行时异常(RuntimeException与其子类)。
15.3 五大运行时异常
-
NullPointerException空指针异常
NullPointerException
是 Java 程序中最常见的异常之一。当试图访问对象的方法或属性,而该对象为空(null)时,就会抛出NullPointerException
。String str = null; int length = str.length(); // 抛出 NullPointerException
-
ArithmeticException算术运算异常
ArithmeticException
表示发生了算术运算错误,通常是因为除以零导致的。int result = 10 / 0; // 抛出 ArithmeticException
-
ArrayIndexOutOfBoundsException数组下标越界异常
ArrayIndexOutOfBoundsException
表示在访问数组元素时,索引超出了数组的范围。通常发生在尝试访问一个不存在的数组元素时。int[] arr = new int[5]; int num = arr[5]; // 抛出 ArrayIndexOutOfBoundsException
-
ClassCastException类型转换异常
ClassCastException
表示试图将对象强制转换为不兼容的类型,导致类型转换失败。Object obj = "hello"; Integer num = (Integer) obj; // 抛出 ClassCastException
-
NumberFormatException数字格式不正确异常
NumberFormatException
字符串的格式不符合数字的语法规范。通常在字符串转换为数字时抛出
15.4 异常处理
异常的捕获
异常捕获处理的方法通常有:
- throws
- throw
- try-catch
- try-catch-finally
- try-finally
- try-with-resource
throws
若方法中存在检查时异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。
在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。
若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常。
public void myMethod() throws IOException, SQLException {
// 方法实现
}
throw
throw
关键字用于在程序中手动抛出异常,表示程序遇到了一些不符合逻辑的情况,需要提前中断并抛出异常。throw
后面通常跟着一个异常对象。
public void myMethod(int num) {
if (num < 0) {
throw new IllegalArgumentException("参数不能为负数");
}
// 方法实现
}
try-catch
在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理
private static void readFile(String filePath) {
try {
// code
} catch (FileNotFoundException e) {
// handle FileNotFoundException
} catch (IOException e){
// handle IOException
}
}
同一个 catch 也可以捕获多种类型异常,用 | 隔开
private static void readFile(String filePath) {
try {
// code
} catch (FileNotFoundException | UnknownHostException e) {
// handle FileNotFoundException or UnknownHostException
} catch (IOException e){
// handle IOException
}
}
try-catch-finally
try {
//执行程序代码,可能会出现异常
} catch(Exception e) {
//捕获异常并处理
} finally {
//必执行的代码
}
- 执行的顺序
- 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
- 当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
- 当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;
try-finally
可以直接用try-finally。
try块中引起异常,异常代码之后的语句不再执行,直接执行finally语句。 try块没有引发异常,则执行完try块就执行finally语句。
try-finally可用在不需要捕获异常的代码,可以保证资源在使用后被关闭。例如IO流中执行完相应操作后,关闭相应资源;使用Lock对象保证线程同步,通过finally可以保证锁会被释放;数据库连接代码时,关闭连接操作等等。
//以Lock加锁为例,演示try-finally
ReentrantLock lock = new ReentrantLock();
try {
//需要加锁的代码
} finally {
lock.unlock(); //保证锁一定被释放
}
注:finally遇见如下情况不会执行
- 在前面的代码中用了System.exit()退出程序。
- finally语句块中发生了异常。
- 程序所在的线程死亡。
- 关闭CPU。
try-with-resource
try-with-resources 是 JDK 7 中一个新的异常处理机制,它能够很容易地关闭在 try-catch 语句块中使用的资源。所谓的资源(resource)是指在程序完成后,必须关闭的对象。try-with-resources 语句确保了每个资源在语句结束时关闭。所有实现了 java.lang.AutoCloseable 接口(其中,它包括实现了 java.io.Closeable 的所有对象),可以使用作为资源。
public static void main(String[] args) {
// 使用 try-with-resources 语句自动关闭 BufferedReader
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
Java 9 对 try-with-resources 做出了改进,允许使用在 try 语句外部已经初始化的 final 或 effectively final 变量(即实际上是 final 的变量,它们在初始化后就不会被再次赋值)。这样做简化了代码,并减少了不必要的冗余。
BufferedReader br = new BufferedReader(new FileReader(path));
try (br) {
// 使用 br 进行读取操作
} catch (IOException e) {
e.printStackTrace();
}
或者,如果你有多个资源需要管理,只要它们都是 final 或 effectively final 的,你也可以这样做:
BufferedReader br = new BufferedReader(new FileReader("path1"));
BufferedWriter bw = new BufferedWriter(new FileWriter("path2"));
try (br; bw) {
// 在这里使用 br 和 bw
} catch (IOException e) {
e.printStackTrace();
}
15.5 自定义异常
在 Java 中,可以通过继承 Exception 或者 RuntimeException 类来创建自定义异常。通常情况下,自定义异常类应该提供有意义的错误信息,以便在程序出错时能够准确地传达问题所在。
- 创建自定义异常类:创建一个继承自 Exception 或者 RuntimeException 的子类,用于表示特定类型的异常。
public class MyCustomException extends Exception {
public MyCustomException(String message) {
super(message);
}
}
- 使用自定义异常类:在程序中需要抛出自定义异常时,创建相应的异常对象并抛出。
public class MyClass {
public void myMethod() throws MyCustomException {
// 某些逻辑...
if (someCondition) {
throw new MyCustomException("发生自定义异常");
}
// 某些逻辑...
}
}
- 捕获自定义异常:在调用可能抛出自定义异常的方法时,需要使用 try-catch 块捕获异常并处理。
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
try {
obj.myMethod();
} catch (MyCustomException e) {
System.out.println("捕获到自定义异常:" + e.getMessage());
}
}
}
📌
一般情况,自定义异常是继承RuntimeException,这样我们可以使用默认的处理机制
十六、常用类详解
16.1 包装类(Wrapper)
Java 的包装类是一组用于将基本数据类型(如 int、double、char 等)封装为对象的类。每个基本数据类型都有对应的包装类,这些包装类位于 java.lang
包中。主要的包装类如下:
包装类 | 基本数据类型 |
---|---|
Integer(父类Number) | int |
Long(父类Number) | long |
Float(父类Number) | float |
Double(父类Number) | double |
Short(父类Number) | short |
Byte(父类Number) | byte |
Character(父类Object) | char |
Boolean(父类Object) | boolean |
这些包装类提供了一系列方法,可以用于将基本数据类型转换为对象,以及在对象之间进行转换、比较和操作。 |
自动装箱和拆箱
Java 5 引入了自动装箱(Autoboxing)和拆箱(Unboxing)机制,使得基本数据类型和其对应的包装类之间的转换更加方便。自动装箱是将基本数据类型自动转换为包装类对象,而自动拆箱则是将包装类对象自动转换为基本数据类型。
Integer num1 = 10; // 自动装箱,相当于 Integer num1 = Integer.valueOf(10);
int num2 = num1; // 自动拆箱,相当于 int num2 = num1.intValue();
包装类的常用方法
valueOf()
:将基本数据类型转换为对应的包装类对象。xxxValue()
:获取包装类对象中的基本数据类型值。toString()
:将包装类对象转换为字符串。parseXxx()
:将字符串转换为基本数据类型。compareTo()
:比较两个包装类对象的大小。equals()
:比较两个包装类对象是否相等。
Integer num1 = Integer.valueOf(10);
int num2 = num1.intValue();
String str = num1.toString();
int num3 = Integer.parseInt("20");
int result = num1.compareTo(num3);
boolean isEqual = num1.equals(num3);
特殊值
每个包装类都有对应的常量表示特殊的值:
MAX_VALUE
:表示最大值。MIN_VALUE
:表示最小值。SIZE
:表示包装类的大小(以比特为单位)。BYTES
:表示包装类的大小(以字节为单位)。
System.out.println("Integer 最大值:" + Integer.MAX_VALUE);
System.out.println("Integer 最小值:" + Integer.MIN_VALUE);
📌
Java 的包装类是不可变类,线程安全的,可以在多线程环境中安全使用。
包装类通常用于集合类和泛型中,因为集合类和泛型只能存储对象,不能存储基本数据类型。此外,还可以用于某些需要对象的API中,如网络编程中的 Socket 类等。
16.2 String字符串
字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。
通过String可以直接定义一个变量,这个变量可以直接就是字符串
String str = "hello";
String类的常用方法
equals
(String str)
:比较当前字符串和参数字符串是否相等,区分大小写。equalsIgnoreCase
(String str)
:比较当前字符串和参数字符串是否相等,不区分大小写。length
:返回当前字符串的字符数量。indexOf
(String str)
:返回参数字符串在当前字符串中第一次出现的索引位置,如果没有找到则返回-1。lastIndexOf
(String str)
:返回参数字符串在当前字符串中最后一次出现的索引位置,如果没有找到则返回-1。substring
(int beginIndex)
:返回从指定索引位置开始到字符串末尾的子字符串。trim
:去除字符串两端的空格字符。charAt
(int index)
:返回指定索引位置的字符。toUpperCase
:将字符串中的所有字符转换为大写形式。toLowerCase
:将字符串中的所有字符转换为小写形式。concat
(String str)
:将参数字符串连接到当前字符串的末尾。compareTo
(String str)
:按字典顺序比较两个字符串,返回一个整数,如果当前字符串小于参数字符串,则返回负值;如果当前字符串大于参数字符串,则返回正值;如果两个字符串相等,则返回0。toCharArray
:将字符串转换为字符数组。format
(String format, Object... args)
:使用指定的格式字符串和参数返回格式化的字符串。split``(String str)
:按指定的分隔符分割字符串。
16.3 StringBuffer
StringBuffer
是 Java 中用于处理可变字符串的类,它允许在字符串中进行插入、追加、删除等操作而不会创建新的字符串对象。这在需要频繁修改字符串内容的情况下很有用,因为直接操作字符串会导致大量的内存重新分配和拷贝,而使用 StringBuffer
可以避免这种性能开销。
📌
StringBuffer的直接父类是AbstractStringBuilder,StringBuffer实现了Serializable,即StringBuffer的对象可以串行化(可以网络传输,可以保存到文件),在父类中有value属性(不是final类型),该属性存放字符串内容,存放在堆中,StringBuffer是一个final类,不能被继承。
StringBuffer的特点
- 可变性(Mutable):与 String 不同,StringBuffer 中的内容可以被修改。可以在已有的 StringBuffer 对象中进行插入、删除、替换等操作,而不会创建新的对象。
- 线程安全性(Thread-Safe):StringBuffer 是线程安全的,即可以在多线程环境下安全地使用,因为它的方法都是同步的(synchronized)。这种同步机制会导致一些性能上的损失,因此在不需要线程安全的情况下,可以使用 StringBuilder,它是 StringBuffer 的非线程安全版本,性能更高。
- 可变大小(Resizable):StringBuffer 的大小可以根据需要自动增长。当向 StringBuffer 中追加内容超出了其初始容量时,它会自动增长以容纳更多的数据,而不需要手动调整大小。
- 方法:StringBuffer 提供了一系列方法来操作字符串,包括增加字符、插入字符、删除字符、替换字符等。常见的方法包括:
append()
,insert()
,delete()
,replace()
,reverse()
等。
StringBuffer的构造器介绍
StringBuffer()
:创建一个初始容量为 16 个字符的字符串缓冲区。StringBuffer(CharSequence seq)
:构造一个字符串缓冲区,将传入的CharSequence
对象的内容复制到新的字符串缓冲区中。StringBuffer(int capacity)
:创建一个具有指定初始容量的字符串缓冲区。初始容量是缓冲区可以包含的字符数,如果需要更多的空间,缓冲区会自动增长。StringBuffer(``String str)
:创建一个包含指定字符串内容的字符串缓冲区。缓冲区的初始容量为原始字符串的长度加上 16
String和StringBuffer的相互转换
String -> StringBuffer
-
使用
StringBuffer
的构造器:String str = "Hello"; StringBuffer stringBuffer = new StringBuffer(str);
-
使用
append()
方法:String str = "Hello"; StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(str);
StringBuffer -> String
-
使用toString方法
StringBuffer stringBuffer = new StringBuffer("Hello"); String str = stringBuffer.toString();
-
使用构造器
StringBuffer stringBuffer = new StringBuffer("Hello");
String str = new String(stringBuffer);
String和StringBuffer对比
Strng保存的是字符串常量(private final char value[]),里面的值不能更改,每次更新实际上是更新地址,效率较低;
StringBuffer保存的是字符串变量,里面的值可以更改,每次更新可以更新内容,不用每次更新地址,效率较高。
StringBuffer类的常用方法
append(String str)
:将指定的字符串追加到字符串缓冲区的末尾。delete(int start,int end)
:从指定位置开始删除到指定位置结束之间的字符。start
是要删除的第一个字符的索引,end
是要删除的最后一个字符的后一个索引。replace(int start,int end,String str)
:用新字符串替换指定范围内的字符。indexOf(String str)
:返回指定子字符串在字符串缓冲区中第一次出现的索引位置。如果没有找到,则返回 -1。insert(int offset,String str)
:在指定位置插入指定的字符串。length()
:返回字符串缓冲区的长度(字符数)。
16.4 StringBuilder
StringBuilder
类与 StringBuffer
类非常相似,都用于处理可变的字符串。它们之间的主要区别在于 StringBuilder
不是线程安全的,而 StringBuffer
是线程安全的。因此,在单线程环境下,通常推荐使用 StringBuilder
,因为它的性能更高。
📌
StringBuilder的直接父类也是AbstractStringBuilder, StringBuilder实现了Serializable,即 StringBuilder的对象可以串行化(可以网络传输,可以保存到文件),在父类中有value属性(不是final类型),该属性存放字符串内容,存放在堆中, StringBuilder是一个final类,不能被继承,StringBuilder的方法都没有synchronized关键字,即不是同步方法。
StringBuilder的特点
- 可变性(Mutable):与 String 类似,StringBuilder 中的内容可以被修改。可以在已有的 StringBuilder 对象中进行插入、删除、替换等操作,而不会创建新的对象。
- 非线程安全性(Non-Thread-Safe):StringBuilder 不像 StringBuffer 那样是线程安全的。它的方法没有被同步(synchronized),因此在多线程环境中使用时需要自己处理线程安全的问题。由于不需要同步机制,StringBuilder 的性能通常比 StringBuffer 更好。
- 可变大小(Resizable):StringBuilder 的大小可以根据需要自动增长。当向 StringBuilder 中追加内容超出了其初始容量时,它会自动增长以容纳更多的数据,而不需要手动调整大小。
- 方法:StringBuilder 提供了一系列方法来操作字符串,包括增加字符、插入字符、删除字符、替换字符等。常见的方法包括:
append()
,insert()
,delete()
,replace()
,reverse()
等。
String、StringBuffer、StringBuilder的比较
特征 | 可变性 | 线程安全性 | 性能 |
---|---|---|---|
String | 不可变字符串 | 线程安全(immutable,共享池) | 低(连接字符串时会创建新的对象) |
StringBuffer | 可变zi’fu’chuan | 线程安全(synchronized,加锁) | 较高(多线程环境最佳) |
StringBuilder | 可变字符串 | 非线程安全(non-synchronized) | 最高(多线程环境最佳) |
📌
如果对String做大量的修改,不要使用String
String虽然效率低,但是复用率高,经常用于字符串不经常变化的情况下(如常量、配置信息等)
16.5 Math
Java中的Math
类提供了一系列用于执行基本数学运算的静态方法。这些方法可以在不创建Math
类的实例的情况下直接调用,因为它们都是静态方法。
- 常数
Math.PI
: 表示圆周率π的常量。Math.E
: 表示自然对数的底数e的常量。
- 基本数学运算
Math.abs(x)
: 返回参数的绝对值。Math.max(x, y)
: 返回两个参数中较大的值。Math.min(x, y)
: 返回两个参数中较小的值。Math.sqrt(x)
: 返回参数的平方根。Math.pow(x, y)
: 返回x的y次方。Math.ceil(x)
: 返回大于或等于参数的最小整数。Math.floor(x)
: 返回小于或等于参数的最大整数。Math.round(x)
: 返回最接近参数的整数。
- 三角函数
Math.sin(x)
: 返回参数的正弦值(x以弧度表示)。Math.cos(x)
: 返回参数的余弦值(x以弧度表示)。Math.tan(x)
: 返回参数的正切值(x以弧度表示)。Math.asin(x)
: 返回参数的反正弦值,结果在 -π/2 到 π/2 之间。Math.acos(x)
: 返回参数的反余弦值,结果在 0 到 π 之间。Math.atan(x)
: 返回参数的反正切值,结果在 -π/2 到 π/2 之间。Math.atan2(y, x)
: 返回由笛卡尔坐标 (x, y) 表示的点与 x 轴之间的角度。
- 对数函数
Math.log(x)
: 返回参数的自然对数。Math.log10(x)
: 返回参数的以10为底的对数。Math.exp(x)
: 返回e的x次方。
- 其他
Math.random()
: 返回一个[0, 1)范围内的随机数。
16.6 Date、Calender、第三代日期类
Date
java.util.Date
类是 Java 中用来表示日期和时间的类。它存储了自1970年1月1日00:00:00 GMT(格林威治标准时间)以来的毫秒数。在很多方面,它已经过时了,因为它存在一些设计缺陷和问题,例如它不是线程安全的,并且提供的方法有限。
SimpleDateFormat
SimpleDateFormat
类是 Java 中用于格式化和解析日期的类之一。它可以将日期对象格式化为指定模式的字符串,以及将字符串解析为日期对象。
示例:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
public static void main(String[] args) {
// 创建一个 SimpleDateFormat 实例,定义日期格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
// 将字符串解析为日期对象
String dateString = "2024-03-04 14:30:00";
Date date = sdf.parse(dateString);
System.out.println("Parsed Date: " + date);
// 将日期对象格式化为字符串
Date currentDate = new Date();
String formattedDate = sdf.format(currentDate);
System.out.println("Formatted Date: " + formattedDate);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
Calender
java.util.Calendar
类是用于处理日期和时间的抽象基类。它提供了许多方法来操作日期和时间字段,例如年、月、日、小时、分钟、秒等。Calendar
类的实例可以用于执行日期和时间算术运算以及格式化日期和时间。
获取当前日期和时间的示例
import java.util.Calendar;
public class Main {
public static void main(String[] args) {
// 获取当前日期和时间的 Calendar 实例
Calendar currentDateTime = Calendar.getInstance();
System.out.println("Current Date and Time: " + currentDateTime.getTime());
}
}
获取某个日历字段的示例
import java.util.Calendar;
public class Main {
public static void main(String[] args) {
// 获取当前日期和时间的 Calendar 实例
Calendar calendar = Calendar.getInstance();
// 获取年份
int year = calendar.get(Calendar.YEAR);
System.out.println("Year: " + year);
// 获取月份(注意:月份从0开始,需要加1)
int month = calendar.get(Calendar.MONTH) + 1;
System.out.println("Month: " + month);
// 获取日期
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println("Day of Month: " + dayOfMonth);
// 获取小时
int hourOfDay = calendar.get(Calendar.HOUR_OF_DAY);
System.out.println("Hour of Day: " + hourOfDay);
// 获取分钟
int minute = calendar.get(Calendar.MINUTE);
System.out.println("Minute: " + minute);
// 获取秒
int second = calendar.get(Calendar.SECOND);
System.out.println("Second: " + second);
// 获取毫秒
int millisecond = calendar.get(Calendar.MILLISECOND);
System.out.println("Millisecond: " + millisecond);
}
}
前两代日期类(主要指 Date
和 Calendar
)存在一些不足之处,这些问题在 Java 8 引入新的日期和时间 API 后得到了解决。以下是前两代日期类的一些主要不足之处:
- 可变性:
Date
和Calendar
类都是可变的(mutable),这意味着您可以直接修改它们的值。这种可变性导致了一些问题,特别是在多线程环境下,可能会引发并发问题。 - 线程安全性:
Date
类是非线程安全的,而Calendar
类虽然提供了一些方法来保证线程安全,但仍然存在一些问题。在多线程环境下,对Date
和Calendar
对象的操作可能会导致意外的结果。 - 设计不清晰:
Date
和Calendar
类的设计相对混乱,方法名称和用法不够直观。例如,Calendar
中的月份是从 0 开始计数的,这可能导致使用时的混淆和错误。 - 可读性差使用:
Date
和Calendar
类进行日期和时间操作时,代码的可读性较差。对于一些简单的操作,代码可能会变得冗长且难以理解。 - 时区处理不足:
Date
类表示一个特定的时间点,但没有时区信息。而Calendar
类可以设置时区,但在处理时区时比较笨拙,并且容易出错。 - 功能受限:
Date
和Calendar
类提供的方法有限,并且不支持一些常见的日期和时间操作,例如计算日期之间的差异、获取下一个工作日等。
第三代日期类
第三代日期类是指 Java 8 引入的新日期和时间 API,位于 java.time
包中。这个 API 提供了一组现代化的日期和时间类,设计更加清晰、易用,并且避免了旧的日期和时间 API 中存在的一些问题。主要的类包括 LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
等
-
LocalDate
import java.time.LocalDate; public class Main { public static void main(String[] args) { // 获取当前日期 LocalDate currentDate = LocalDate.now(); System.out.println("Current Date: " + currentDate); } }
-
LocalTime
import java.time.LocalTime; public class Main { public static void main(String[] args) { // 获取当前时间 LocalTime currentTime = LocalTime.now(); System.out.println("Current Time: " + currentTime); } }
-
LocalDateTime
import java.time.LocalDateTime; public class Main { public static void main(String[] args) { // 获取当前日期和时间 LocalDateTime currentDateTime = LocalDateTime.now(); System.out.println("Current Date and Time: " + currentDateTime); } }
-
ZonedDateTime
import java.time.ZonedDateTime; public class Main { public static void main(String[] args) { // 获取当前日期和时间以及时区信息 ZonedDateTime currentDateTimeWithZone = ZonedDateTime.now(); System.out.println("Current Date and Time with Zone: " + currentDateTimeWithZone); } }
这些类提供了丰富的方法来操作日期和时间,例如比较、格式化、解析、计算等。它们的设计更加清晰和灵活,并且避免了旧的日期和时间 API 中的一些问题,如线程安全性、可读性等。因此,在编写新的日期和时间相关的代码时,建议使用这些新的日期和时间 API。
-
DateTimeFormatter
DateTimeFormatter
类是 Java 中用于格式化和解析日期时间对象的类。它属于 Java 8 引入的新日期和时间 API (java.time.format
包),与SimpleDateFormat
类类似,但提供了更强大、更灵活的功能。
以下是一些常用的DateTimeFormatter
方法和模式符号:
ofPattern(String pattern)
:创建一个新的DateTimeFormatter
实例,指定日期时间的格式模式。format(TemporalAccessor temporal)
:将日期时间对象格式化为字符串。parse(CharSequence text)
:将字符串解析为日期时间对象。
常见的模式符号包括:yyyy
:四位年份MM
:两位月份dd
:两位日期HH
:24小时制的小时数hh
:12小时制的小时数mm
:分钟ss
:秒SSS
:毫秒Z
:时区偏移量(例如 +0800)
示例
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Main {
public static void main(String[] args) {
// 创建一个 DateTimeFormatter 实例,定义日期时间格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化日期时间对象为字符串
LocalDateTime dateTime = LocalDateTime.now();
String formattedDateTime = formatter.format(dateTime);
System.out.println("Formatted Date and Time: " + formattedDateTime);
// 将字符串解析为日期时间对象
String strDateTime = "2024-03-04 14:30:00";
LocalDateTime parsedDateTime = LocalDateTime.parse(strDateTime, formatter);
System.out.println("Parsed Date and Time: " + parsedDateTime);
}
}
16.7 System
Java中的System类是一个包含了一系列静态方法和字段的内置类,主要用于处理与系统相关的操作。它位于java.lang
包中,因此无需导入即可直接使用。System类的一些主要功能包括:管理标准输入输出、访问系统属性、执行垃圾回收、与外部环境交互等。
一些System类的常用方法和字段
- 标准输入输出
public static final InputStream in
:标准输入流,通常用于从键盘获取用户输入。public static final PrintStream out
:标准输出流,通常用于向控制台输出信息。public static final PrintStream err
:标准错误输出流,通常用于输出错误信息。
- 系统属性
public static String getProperty(String key)
:获取系统属性的值,通过传入属性名作为参数,例如System.getProperty("java.version")
可以获取Java运行时的版本信息。public static Properties getProperties()
:获取所有系统属性,返回一个Properties对象,包含了系统的所有属性键值对。
- 垃圾回收
public static void gc()
:强制进行垃圾回收。尽管Java有自动垃圾回收机制,但是可以通过此方法强制触发垃圾回收。
- 退出程序
public static void exit(int status)
:终止当前正在运行的Java虚拟机。参数status
通常用于表示程序的退出状态,约定0表示正常退出,非0表示异常退出。
- 数组复制
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
:用于将一个数组中的指定区域复制到另一个数组中。参数分别表示源数组、源数组起始位置、目标数组、目标数组起始位置以及要复制的长度。
- 当前时间
public static long currentTimeMillis()
:获取当前时间的毫秒数,通常用于性能测试和计时。
- 安全管理器
public static SecurityManager getSecurityManager()
:获取安全管理器,用于实施安全策略。
16.8 Arrays
Java中的Arrays
类提供了一系列用于操作数组的静态方法。这些方法对数组进行排序、搜索、填充等操作,以及创建包含数组内容的字符串表示。
以下是Arrays
类的一些常用方法:
-
排序
-
sort(array)
: 对数组进行升序排序。 -
sort(array, comparator)
: 使用指定的比较器对数组进行排序。Arrays.sort(arr, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); ```* `parallelSort(array)`: 使用并行算法对数组进行排序。
-
-
搜索
binarySearch(array, key)
: 使用二分搜索算法在已排序数组中查找指定的元素。binarySearch(array, key, comparator)
: 使用指定的比较器在已排序数组中查找指定的元素。
-
填充
fill(array, value)
: 将指定值填充到数组的所有元素中。fill(array, fromIndex, toIndex, value)
: 将指定值填充到数组的指定范围内的元素中。
-
比较
equals(array1, array2)
: 比较两个数组是否相等。deepEquals(array1, array2)
: 深度比较两个数组是否相等(对于多维数组)。
-
转换
toString(array)
: 返回数组的字符串表示形式。deepToString(array)
: 返回深度数组的字符串表示形式。asList(T... a)
: 将数组转换为List集合返回。
-
复制
copyOf(original, length)
: 复制数组的指定长度。copyOfRange(original, fromIndex, toIndex)
: 复制数组的指定范围。
-
填充和复制原始类型数组
- 对于
boolean
、byte
、char
、short
、int
、long
、float
和double
类型,Arrays
类提供了相应的方法,如fill()
,copyOf()
,copyOfRange()
等,用于操作原始类型数组。
- 对于
16.9 BigInteger、BigDecimal
Java中的BigInteger
和BigDecimal
类用于处理大整数和大浮点数,分别位于java.math
包中。它们的出现是为了解决Java原生数据类型所不能表示的大数字运算问题,例如超出了long
类型的范围或需要高精度的小数计算。
1. BigInteger
BigInteger
类用于表示任意精度的整数。它是不可变的(immutable)的,一旦创建,就不能修改其值。以下是一些BigInteger
类的主要方法和用法:
- 构造方法
BigInteger(String val)
:通过字符串创建BigInteger
对象。
- 常量
BigInteger.ZERO
:表示值为0的BigInteger
。BigInteger.ONE
:表示值为1的BigInteger
。BigInteger.TEN
:表示值为10的BigInteger
。
- 运算方法
add(BigInteger val)
:将当前BigInteger
与另一个BigInteger
相加。subtract(BigInteger val)
:将当前BigInteger
减去另一个BigInteger
。multiply(BigInteger val)
:将当前BigInteger
与另一个BigInteger
相乘。divide(BigInteger val)
:将当前BigInteger
除以另一个BigInteger
,返回商。remainder(BigInteger val)
:返回当前BigInteger
除以另一个BigInteger
的余数。pow(int exponent)
:返回当前BigInteger
的幂。
- 比较方法
compareTo(BigInteger val)
:将当前BigInteger
与另一个BigInteger
进行比较,返回负数、零或正数,表示小于、等于或大于。
2. BigDecimal
BigDecimal
类用于表示任意精度的浮点数,适用于金融和其他需要精确计算的领域。与BigInteger
类似,BigDecimal
也是不可变的。以下是一些BigDecimal
类的主要方法和用法:
- 构造方法
BigDecimal(String val)
:通过字符串创建BigDecimal
对象。
- 运算方法
add(BigDecimal val)
:将当前BigDecimal
与另一个BigDecimal
相加。subtract(BigDecimal val)
:将当前BigDecimal
减去另一个BigDecimal
。multiply(BigDecimal val)
:将当前BigDecimal
与另一个BigDecimal
相乘。divide(BigDecimal val)
:将当前BigDecimal
除以另一个BigDecimal
,返回商。remainder(BigDecimal val)
:返回当前BigDecimal
除以另一个BigDecimal
的余数。pow(int exponent)
:返回当前BigDecimal
的幂。
- 比较方法
compareTo(BigDecimal val)
:将当前BigDecimal
与另一个BigDecimal
进行比较,返回负数、零或正数,表示小于、等于或大于。
- 设置精度
setScale(int newScale, RoundingMode roundingMode)
:设置BigDecimal
对象的精度和舍入模式。
- 其他方法
abs()
:返回当前BigDecimal
的绝对值。negate()
:返回当前BigDecimal
的相反数。
BigDecimal
类还提供了许多其他方法,用于执行各种数学运算和操作。它的主要优势在于能够保持高精度,避免了浮点数计算中的舍入误差问题,因此在需要精确计算的场景中应用广泛,如金融领域的利率计算、货币转换等。
十七、集合
17.1 集合体系介绍
Java的集合体系是Java编程语言中非常重要的一部分,它提供了一组框架和接口,用于存储、操作和处理数据。
集合体系图
- 单列集合
- List:List是有序集合,允许重复元素,可以根据索引访问集合中的元素。
- Set:Set是无序集合,不允许重复元素,确保集合中不含有相同的元素。
- Queue:Queue是一种先进先出(FIFO)的数据结构,用于存储一组元素,通常用于实现队列。
- 双列集合
- Map:Map是一种将键映射到值的集合,每个键都唯一,但值可以重复。可以根据键来获取对应的值,也可以根据值来查找对应的键。
17.2 Collection
1. Collection接口介绍
Collection接口中的常用方法
-
add(E element)
:将指定的元素添加到列表的尾部。 -
add(int index, E element)
:将指定的元素插入到列表的指定位置。 -
addAll(Collection<? extends E> c)
:将指定集合中的所有元素添加到列表的尾部。 -
addAll(int index, Collection<? extends E> c)
:将指定集合中的所有元素插入到列表的指定位置。 -
clear()
:移除列表中的所有元素。 -
contains(Object o)
:如果列表包含指定的元素,则返回true。 -
containsAll(Collection<?> c)
:检查当前列表是否包含另一个集合中的所有元素。 -
get(int index)
:返回列表中指定位置的元素。 -
indexOf(Object o)
:返回列表中指定元素的第一个匹配项的索引,如果列表中不包含该元素,则返回-1。 -
isEmpty()
:如果列表不包含任何元素,则返回true。 -
remove(Object o)
:移除列表中第一个出现的指定元素(如果存在)。 -
remove(int index)
:移除列表中指定位置的元素。 -
removeAll(Collection<?> c)
:从当前列表中移除另一个集合中的所有元素。 -
size()
:返回列表中的元素数。 -
set(int index, E element)
:用指定的元素替换列表中指定位置的元素。 -
subList(int fromIndex, int toIndex)
:返回列表中指定范围的部分视图。 -
toArray()
:将列表转换为数组。
Collection遍历元素
-
使用迭代器Iterator
- 迭代器是遍历集合的常见方式,它提供了
hasNext()
和next()
方法来遍历集合中的元素,并且支持在遍历过程中删除元素。 - 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器。
Collection<String> collection = new ArrayList<>(); collection.add("A"); collection.add("B"); collection.add("C"); Iterator<String> iterator = collection.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(element); } //如果需要再次便利,需要重置迭代器 //iterator = collection.iterator(); ```2. 使用增强for循环 * 增强型for循环(也称为for-each循环)可以方便地遍历集合中的元素,不需要显式地使用迭代器。 * 增强for循环底层也是使用Iterator ```java Collection<String> collection = new ArrayList<>(); collection.add("A"); collection.add("B"); collection.add("C"); for (String element : collection) { System.out.println(element); } ```3. 使用Java 8的Stream API * Java 8引入了Stream API,它提供了强大的功能来操作集合和数组。通过Stream API,可以使用 `forEach()` 方法来遍历集合中的元素。 ```java Collection<String> collection = new ArrayList<>(); collection.add("A"); collection.add("B"); collection.add("C"); collection.stream().forEach(System.out::println);
- 迭代器是遍历集合的常见方式,它提供了
2. List接口介绍
以下是Java中List接口的一些常用方法:
-
add(int index, E element)
:将指定的元素插入到列表中的指定位置。原来位于该位置的元素以及所有后续元素都会向右移动(索引加一)。 -
boolean addAll(int index, Collection<? extends E> c)
:将指定集合中的所有元素插入到列表中的指定位置。原来位于该位置的元素以及所有后续元素都会向右移动(索引增加c.size()
)。 -
E get(int index)
:返回列表中指定位置的元素。 -
int indexOf(Object o)
:返回列表中首次出现的指定元素的索引,如果列表中不包含此元素,则返回 -1。 -
int lastIndexOf(Object o)
:返回列表中最后出现的指定元素的索引,如果列表中不包含此元素,则返回 -1。 -
ListIterator<E> listIterator()
:返回列表中的列表迭代器(按适当的顺序)。 -
ListIterator<E> listIterator(int index)
:返回列表中的列表迭代器,从列表中的指定位置开始。 -
E remove(int index)
:移除列表中指定位置的元素。原来位于该位置后面的元素都会向前移动(索引减一)。 -
E set(int index, E element)
:用指定的元素替换列表中指定位置的元素。 -
List<E> subList(int fromIndex, int toIndex)
:返回列表中指定的 fromIndex(包括 )到 toIndex(不包括)之间的部分视图。
List-ArrayList
ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素,ArrayList是通过数组实现数据存储的。
ArrayList 继承了 AbstractList ,并实现了 List 接口。
ArrayList 类位于 java.util 包中,使用前需要引入它,语法格式如下:
import java.util.ArrayList; // 引入 ArrayList 类
ArrayList<E> objectName =new ArrayList<>(); // 初始化
- E: 泛型数据类型,用于设置 objectName 的数据类型,只能为引用数据类型。
- objectName: 对象名。
ArrayList 的主要特点
- 数组存储元素
ArrayList 内部使用一个 Object 类型的数组来存储元素。这个数组在初始化时有一个默认的容量,通常是 10 个元素,可以通过构造函数指定初始容量。 - 动态扩容
当 ArrayList 的元素个数达到当前容量时,会触发扩容操作。扩容操作一般会创建一个新的数组,其大小是原数组的 1.5 倍(具体倍数可能根据实现不同而有所差异),然后将原数组中的元素复制到新数组中。这个操作的时间复杂度是 O(n),其中 n 是当前数组的大小。 - 元素的增删操作
ArrayList 提供了添加、删除和修改元素的方法。在数组末尾添加元素的操作是最高效的,因为它只需要将元素放入数组的末尾,并且不会触发扩容操作。删除元素或者在中间插入元素会涉及到数组元素的移动,效率较低,尤其是在大量操作时。删除元素时,ArrayList 会将删除位置之后的所有元素向前移动一个位置,并将最后一个元素置为 null,以便垃圾回收。 - 随机访问
由于 ArrayList 基于数组实现,所以支持随机访问。通过索引即可快速访问到指定位置的元素,时间复杂度为 O(1)。 - 迭代器
ArrayList 实现了 Iterable 接口,可以通过迭代器来遍历集合中的元素。
ArrayList扩容机制
ArrayList 内部使用一个 Object 类型的数组来存储元素。
如果是使用的无参构造初始化ArrayList,这个数组在初始化时,容量为0,在添加第一个元素时会进行一次扩容,将初始容量扩展为默认容量(默认是 10),可以通过构造函数指定初始容量,如果添加的元素数量超过了初始容量,ArrayList 会进行动态扩容,通常是扩容为原来容量的 1.5 倍。
如果使用的是指定大小的构造器,则初始容量为指定的容量,如果需要扩容,则直接扩容当前容量的1.5倍。
底层扩容使用的是copyOf方法。
ArrayList 常用方法列表
方法 | 描述 |
---|---|
add() | 将元素插入到指定位置的 arraylist 中 |
addAll() | 添加集合中的所有元素到 arraylist 中 |
clear() | 删除 arraylist 中的所有元素 |
clone() | 复制一份 arraylist |
contains() | 判断元素是否在 arraylist |
get() | 通过索引值获取 arraylist 中的元素 |
indexOf() | 返回 arraylist 中元素的索引值 |
removeAll() | 删除存在于指定集合中的 arraylist 里的所有元素 |
remove() | 删除 arraylist 里的单个元素 |
size() | 返回 arraylist 里元素数量 |
isEmpty() | 判断 arraylist 是否为空 |
subList() | 截取部分 arraylist 的元素 |
set() | 替换 arraylist 中指定索引的元素 |
sort() | 对 arraylist 元素进行排序 |
toArray() | 将 arraylist 转换为数组 |
toString() | 将 arraylist 转换为字符串 |
ensureCapacity | 设置指定容量大小的 arraylist |
lastIndexOf() | 返回指定元素在 arraylist 中最后一次出现的位置 |
retainAll() | 保留 arraylist 中在指定集合中也存在的那些元素 |
containsAll() | 查看 arraylist 是否包含指定集合中的所有元素 |
trimToSize() | 将 arraylist 中的容量调整为数组中的元素个数 |
removeRange() | 删除 arraylist 中指定索引之间存在的元素 |
replaceAll() | 将给定的操作内容替换掉数组中每一个元素 |
removeIf() | 删除所有满足特定条件的 arraylist 元素 |
forEach() | 遍历 arraylist 中每一个元素并执行特定操作 |
📌
ArrayList基本等同于Vector,不同点是ArrayList是线程不安全的
List-Vector
Vector
类是 Java 中的一个线程安全的动态数组实现,它与 ArrayList
类似,但是在操作上提供了线程安全的功能。
- Vector底层也是一个对象数组,
protected Object[] elementData;
- Vector是线程同步的,在开发中,如果需要线程同步安全时,考虑使用Vector
Vector 的主要特点
- 线程安全性
Vector
Vector
是线程安全的,这意味着多个线程可以同时访问一个 Vector 实例,而不需要额外的同步措施。它通过在每个公共方法上添加同步关键字来实现线程安全。这使得在多线程环境中对 Vector 的操作更加安全,但也会带来一定的性能开销。 - 底层实现
VectorArrayListVector
Vector
也是基于数组实现的动态数组。它内部使用一个 Object 类型的数组来存储元素。与ArrayList
类似,Vector
也会在需要时进行扩容操作,增长策略也类似,通常是当前容量的 2 倍。 - 增删改查操作
VectorArrayListArrayListVector
Vector
提供了添加、删除、修改和获取元素的方法,与ArrayList
类似。但由于它是线程安全的,因此在进行这些操作时会涉及到同步措施,可能会影响性能。与ArrayList
不同,Vector
的所有方法都是同步的,即使只有一个线程访问它,也会执行同步操作。 - 迭代器
Vector
Vector
也实现了 Iterable 接口,可以通过迭代器来遍历集合中的元素。 - 遗留特性
VectorArrayList
Vector
是 JDK 1.0 中引入的,它的许多设计考虑了早期 Java 版本中的一些限制和需求。因此,它在某些方面可能不如ArrayList
灵活和高效。例如,它的所有方法都是同步的,这在单线程环境中可能会带来额外的开销。
📌
Vector
适用于多线程环境中需要线程安全的动态数组操作,但在单线程环境中,由于额外的同步开销,可能不如ArrayList
高效。随着 Java 平台的发展,通常建议优先使用ArrayList
,并在需要线程安全时考虑使用Collections.synchronizedList
或者更高级的并发集合类来代替Vector
。
Vector扩容机制
Vector 是 Java 中的一个线程安全的动态数组,它可以根据需要自动增长。Vector 的扩容机制是在需要添加元素时,如果当前元素个数已经达到了内部数组的容量,则会创建一个新的数组,容量通常是原来数组容量的两倍,然后将原来数组中的元素复制到新数组中,最后将新元素添加到新数组中。
具体的扩容过程如下:
- 当添加元素时,首先检查当前元素个数是否达到了内部数组的容量。
- 如果当前元素个数达到了容量上限,则需要进行扩容。
- 创建一个新的数组,通常容量是原来数组容量的两倍。
- 将原来数组中的元素逐个复制到新数组中。
- 将新元素添加到新数组中。
- 将内部数组引用指向新数组。
Vector 使用了 ensureCapacity(int minCapacity)
方法来确保容量,该方法会检查当前容量是否足够存放至少 minCapacity 个元素,如果不够则进行扩容。
由于 Vector 是线程安全的,因此在进行扩容时会使用同步机制来保证线程安全性,这可能会导致在多线程环境下性能下降。如果不需要线程安全性,可以考虑使用 ArrayList 来替代 Vector,ArrayList 是非线程安全的动态数组,性能更好。
List-LinkedList
LinkedList
类是 Java 中的一个双向链表实现,它实现了 List
接口。与 ArrayList
不同,LinkedList
的底层数据结构是链表,而不是数组。
下面详细讲解一下 LinkedList
类的特点和使用方法:
LinkedList的主要特点
- 双向链表结构
LinkedList
内部采用双向链表结构来存储元素。每个节点都包含对前一个节点和后一个节点的引用。这种结构使得在插入和删除元素时更加高效,因为只需要调整相邻节点的引用,而不需要像数组那样移动大量元素。 - 不是线程安全的
与Vector
不同,LinkedList
不是线程安全的,因此在多线程环境下使用时需要考虑同步问题。如果需要在多线程环境中安全地使用LinkedList
,可以通过Collections.synchronizedList()
方法或者使用并发集合类来实现线程安全。 - 随机访问效率低
由于LinkedList
的底层是链表结构,因此在进行随机访问时效率较低。要获取链表中的第 n 个元素,需要从头结点或尾结点开始遍历,直到找到目标节点。因此,LinkedList
不适合需要频繁进行随机访问的场景。 - 增删操作高效
由于链表的特点,LinkedList
在插入和删除元素时效率较高。在链表中插入或删除一个节点,只需要修改节点的前后节点的引用,而不需要像数组那样移动大量元素。 - 迭代器
LinkedList
实现了Iterable
接口,因此可以使用迭代器来遍历集合中的元素。由于链表的结构,迭代器可以双向移动,因此在遍历时可以从头到尾或者从尾到头遍历。 - 支持队列和栈操作
由于链表的结构特点,LinkedList
也可以用作队列或者栈。可以使用addFirst()
和addLast()
方法在链表的头部或尾部添加元素,以及使用removeFirst()
和removeLast()
方法从头部或尾部移除元素,实现队列和栈的操作。
📌
LinkedList
适用于需要频繁进行插入和删除操作,而对随机访问要求不高的场景。如果需要频繁进行随机访问,或者需要线程安全的动态数组,通常会选择ArrayList
或者Vector
。
ArrayList、Vector、LinkedList对比
特性 | ArrayList | LinkedList | Vector |
---|---|---|---|
底层数据结构 | 动态数组 | 双向链表 | 动态数组 |
线程安全性 | 非线程安全 | 非线程安全 | 线程安全 |
扩容策略 | 每次扩容为当前容量的1.5倍 | 不需要扩容 | 每次扩容为当前容量的2倍 |
遍历效率 | 高效(随机访问) | 低效(顺序访问,随机访问需要遍历) | 高效(随机访问) |
插入/删除效率 | 尾部插入/删除高效,中间插入/删除低效 | 插入/删除高效(头尾操作) | 尾部插入/删除高效,中间插入/删除低效 |
内存占用 | 相对较小(只占用实际元素的内存 + 一些额外) | 相对较大(每个元素需要额外的链表节点) | 相对较小(只占用实际元素的内存 + 一些额外) |
随机访问时间 | O(1) | O(n) | O(1) |
3. Set接口介绍
Java中的Set接口是一种集合,它代表了一组不包含重复元素的无序集合。
Set接口的主要特点
- 不包含重复元素:Set中不允许存储重复的元素。如果尝试将一个已经存在于Set中的元素插入Set,插入操作将会被忽略。
- 无序性:Set接口不保证元素的顺序。具体实现类可能会维护元素的插入顺序,也可能不会。
- 不允许使用索引:与List不同,Set接口没有提供通过索引来访问元素的方法,因为元素是无序的。
📌
和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样
Set-HashSet
HashSet是Java中Set接口的一个常见实现类,它基于哈希表实现,提供了高效的插入、删除和查找操作。HashSet的底层其实是HashMap。
下面是对HashSet的详细介绍:
HashSet的主要特点
- 不包含重复元素:HashSet不允许存储重复的元素。如果尝试将一个已经存在于HashSet中的元素插入HashSet,插入操作将会被忽略。
- 无序性:HashSet中的元素没有指定的顺序。具体的实现可能会维护元素的插入顺序,也可能不会。注:无序是指添加的顺序和取出的顺序不一致,而不是说每一次执行程序有不同的顺序,取出顺序虽然不是添加的顺序,但是它是固定的。
- 基于哈希表实现:HashSet内部使用一个哈希表来存储元素,通过哈希算法将元素映射到哈希表的某个位置。这使得插入、删除和查找元素的时间复杂度都是常数级别的,即O(1)。
- 允许存储null元素:HashSet允许存储一个null元素。
- 非线程安全:HashSet是非线程安全的,如果需要在多线程环境中使用,可以考虑使用Collections类提供的synchronizedSet()方法创建线程安全的HashSet,或者使用并发集合类如ConcurrentHashMap。
方法
HashSet实现了Set接口,因此具有Set接口定义的所有方法,主要包括:
add(E e)
:向HashSet中添加元素。remove(Object o)
:从HashSet中移除指定的元素。contains(Object o)
:判断HashSet中是否包含指定的元素。size()
:返回HashSet中元素的数量。isEmpty()
:判断HashSet是否为空。clear()
:清空HashSet中的所有元素。iterator()
:返回一个迭代器,用于遍历HashSet中的元素。
使用示例:
import java.util.HashSet;
public class HashSetExample {
public static void main(String[] args) {
// 创建一个HashSet对象
HashSet<String> set = new HashSet<>();
// 添加元素
set.add("apple");
set.add("banana");
set.add("orange");
// 判断元素是否存在
System.out.println(set.contains("apple")); // true
// 移除元素
set.remove("banana");
// 获取元素数量
System.out.println(set.size()); // 2
// 遍历元素
for (String fruit : set) {
System.out.println(fruit);
}
}
}
HashSet底层机制详解
-
HashSet底层是HashMap,第一次添加时数组扩容到16,临界值(threshold)是16x加载因子(loadFactor)是0.75 = 12(只要增加一个元素,就算一个),当table数组满了12个后,就会扩容到16x2 = 32个,新的临界值就是32x0.75 = 24
-
添加一个元素时,先得到hash值,再转成索引值
-
找到存储表table,看这个索引位置是否有元素
-
如果没有,直接加入
-
如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后
-
再Java8中,如果一条链表的元素个数超过
TREEIFY_THRESHOLD
(默认为8),并且table的大小MIN_TREEIFY_CAPACITY
(默认为64)就会进行树化,否则仍然采用数组进行扩容
更详细的机制参考:
HashSet(HashMap)底层原码剖析详解 https://blog.csdn.net/m0_54068390/article/details/137056523
Set-LinkedHashSet
LinkedHashSet是Java中的一个集合类,它是HashSet的子类,实现了Set接口。与HashSet不同的是,LinkedHashSet底层是LinkedHashMap,底层维护了一个数组+双向链表,并且通过链表维护元素的顺序,使其取出时是按插入顺序取出。
LinkedHashSet的主要特点
- 插入顺序: LinkedHashSet会按照元素插入的顺序来维护元素的顺序。这意味着当你遍历LinkedHashSet时,元素的顺序将与它们插入时的顺序相同。
- 唯一性:LinkedHashSet与其他Set一样,保证其中的元素是唯一的。如果尝试向LinkedHashSet中添加一个已经存在的元素,添加操作将被忽略。
- 性能 :LinkedHashSet的性能与HashSet类似。添加、删除和查找元素的性能都是常数时间(O(1))。但是,由于维护了一个链表,LinkedHashSet在遍历时略微慢于HashSet。
- 线程不安全:LinkedHashSet不是线程安全的,如果在多线程环境下使用LinkedHashSet,你需要自行处理同步问题。
- Null元素: LinkedHashSet允许添加一个null元素,但是只能添加一个,因为它是一个Set,不允许重复元素。
📌
LinkedHashSet与HashSet的主要区别就是LinkedHashSet底层维护了一个数组+双向链表
Set-TreeSet
TreeSet是Java中的一个集合类,它实现了Set接口,它的底层其实是TreeMap,同时基于树的数据结构实现了有序的集合。
以下是TreeSet集合的详细介绍:
TreeSet的主要特点
- 有序性
TreeSet是有序的集合,它根据元素的自然顺序进行排序,或者根据在构造集合时提供的Comparator进行排序。这使得TreeSet中的元素总是按照特定的顺序排列。 - 基于树的数据结构
TreeSet内部使用一棵红黑树(Red-Black Tree)来存储元素。红黑树是一种自平衡的二叉查找树,它确保了插入、删除和搜索等操作的时间复杂度为O(log n),其中n是集合中元素的数量。 - 不允许重复元素
TreeSet不允许集合中存在重复的元素。如果试图向TreeSet中添加已经存在的元素,那么该元素不会被添加,集合的大小也不会发生变化。 - 元素的比较
TreeSet中的元素必须是可比较的,即它们必须实现Comparable接口或者在创建TreeSet时提供一个Comparator来进行比较。如果元素没有实现Comparable接口,并且没有提供Comparator,则在尝试插入元素时会抛出ClassCastException异常。 - 性能
由于TreeSet基于红黑树实现,因此它的插入、删除和搜索操作的时间复杂度为O(log n),其中n是集合中元素的数量。这使得TreeSet适用于需要高效搜索和有序存储元素的场景。 - 线程不安全
TreeSet是非线程安全的,如果多个线程同时访问一个TreeSet,并且至少有一个线程在结构上修改了集合,那么必须通过外部同步来确保TreeSet的线程安全性。 - 遍历
TreeSet提供了多种遍历集合的方法,包括迭代器、forEach循环等。由于TreeSet是有序的集合,因此遍历时元素的顺序是确定的,可以按照升序或降序遍历。
📌
TreeSet适用于需要存储不重复元素并保持有序的场景,但需要注意它的线程不安全性
HashSet和TreeSet分别是如何实现去重的
- HashSet的去重是通过hashCode() + equals()实现的,底层先通过存入对象,进行运算得到一个hash值,通过hash值得到对应的索引,如果存储表table的该索引位置没有元素,则直接添加,如果有数据,就进行equals比较,如果比较后不相同,就添加到链表或红黑树的尾部,否则就不加入。由于元素可能是以链表或红黑树形式存放,这里的比较会进行遍历。
- TreeSet的去重机制。如果传入了一个Comparator匿名对象,就是用该匿名类内部的compare方法去重,如果方法返回0,就认为是相同的元素或数据,就不添加,如果没有传入Comparator匿名对象,就按照添加的对象实现的Comparable接口的compareTo方法进行去重
17.3 Map
1. Map接口介绍
Map接口的特点
- Map用于保存具有映射关系的数据,保存为Key-Value
- Map中的Key和Value可以是任意引用类型的数据,会封装到HashMap$Node对象中
- Map中的key不允许重复,原因和HashSet一样,key重复添加会导致value修改
- Map中的value可以重复
- Map的key可以为null,value也可以为null,注意key为null,只能有一个,value为null,可以有多个
- 常用的String类作为Map的key
- key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
- 一对k-v是放在一个Node的,因为Node实现了Entry接口,有些书上也说一对k-v就是一个Entry
Map接口的方法
下面是Map
接口的常用方法:
put(K key, V value)
:将指定的键值对添加到 Map 中。如果键已经存在,则用新值替换旧值,并返回旧值;如果键不存在,则返回null
。get(Object key)
:返回指定键所映射的值,如果 Map 中不包含该键,则返回null
。containsKey(Object key)
:如果 Map 中包含指定键,则返回true
;否则返回false
。containsValue(Object value)
:如果 Map 中包含一个或多个键映射到指定值,则返回true
;否则返回false
。remove(Object key)
:从 Map 中移除指定键及其对应的值,并返回该键对应的值。size()
:返回 Map 中键值对的数量。isEmpty()
:如果 Map 中不包含任何键值对,则返回true
;否则返回false
。clear()
:从 Map 中移除所有的键值对。keySet()
:返回包含 Map 中所有键的集合。values()
:返回包含 Map 中所有值的集合。entrySet()
:返回包含 Map 中所有键值对的集合,每个元素都是Map.Entry
类型。putAll(Map<? extends K, ? extends V> m)
:将指定 Map 中的所有键值对添加到当前 Map 中。
Map遍历元素
- 使用迭代器(Iterator)
Map<String, Integer> map = new HashMap<>();
// 添加键值对到 map 中
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
// 处理键值对
}
```
- 使用增强型 for 循环
Map<String, Integer> map = new HashMap<>();
// 添加键值对到 map 中
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
// 处理键值对
}
- 使用 forEach 方法
Map<String, Integer> map = new HashMap<>();
// 添加键值对到 map 中
map.forEach((key, value) -> {
// 处理键值对
});
- 使用流操作
Map<String, Integer> map = new HashMap<>();
// 添加键值对到 map 中
map.entrySet().stream().forEach(entry -> {
String key = entry.getKey();
Integer value = entry.getValue();
// 处理键值对
});
- 使用keySet获取键并遍历
Map<String, Integer> map = new HashMap<>();
// 假设向 map 中添加了键值对
Set<String> keys = map.keySet(); // 获取所有键
// 使用迭代器遍历
Iterator<String> iterator = keys.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
Integer value = map.get(key); // 获取键对应的值
// 处理键值对
}
// 使用增强型 for 循环遍历
for (String key : keys) {
Integer value = map.get(key); // 获取键对应的值
// 处理键值对
}
// 使用 forEach 方法遍历
keys.forEach(key -> {
Integer value = map.get(key); // 获取键对应的值
// 处理键值对
});
// 使用流操作遍历
keys.stream().forEach(key -> {
Integer value = map.get(key); // 获取键对应的值
// 处理键值对
});
2. HashMap
HashMap
是 Java 中实现了 Map
接口的哈希表数据结构,它提供了一种高效的存储和检索键值对的方法。以下是 HashMap
的详细解释:
-
HashMap数据结构
HashMap
基于哈希表实现,它采用数组 + 链表(或红黑树)的数据结构来存储键值对。数组的每个元素称为桶(bucket),每个桶存储一个链表(或红黑树),用于处理哈希冲突。 -
哈希函数
HashMap
使用键的哈希码(通过调用键对象的hashCode()
方法)来确定键值对的存储位置。哈希码通过哈希函数映射到数组中的索引位置。 -
解决哈希冲突
当多个键映射到相同的数组索引位置时,即发生哈希冲突。HashMap
使用链表(或红黑树)来解决冲突:- JDK 8 之前:使用链表。在同一个桶中的键值对被组织成一个链表,新的键值对被添加到链表的末尾。
- JDK 8 及之后:在链表长度超过一定阈值后,将链表转换为红黑树,以提高检索效率。
-
HashMap主要方法
HashMap
提供了一系列方法来操作键值对:
put(K key, V value)
: 将指定的键值对添加到HashMap
中。get(Object key)
: 返回指定键对应的值。remove(Object key)
: 移除指定键及其对应的值。containsKey(Object key)
: 检查HashMap
是否包含指定的键。containsValue(Object value)
: 检查HashMap
是否包含指定的值。keySet()
: 返回HashMap
中所有键的集合。values()
: 返回HashMap
中所有值的集合。entrySet()
: 返回HashMap
中所有键值对的集合。
-
HashMap特点
- 键不允许重复,每个键对应唯一的值。
- 键和值都可以为
null
,但只能有一个键为null
。 HashMap
不是线程安全的,如果需要在多线程环境中使用,可以考虑使用ConcurrentHashMap
。- 在大多数情况下,
HashMap
具有 O(1) 的时间复杂度,但在最坏情况下,可能会达到 O(n)。
-
HashMap扩容机制
HashMap
的扩容机制就是上面提到的HashSet的扩容机制(HashSet底层就是HashMap)
HashMap底层机制详解
1. HashMap第一次添加时数组扩容到16,临界值(threshold)是16x加载因子(loadFactor)是0.75 = 12(只要增加一个元素,就算一个),当table数组满了12个后,就会扩容到16x2 = 32个,新的临界值就是32x0.75 = 24
2. 添加一个元素时,先得到hash值,再转成索引值
3. 找到存储表table,看这个索引位置是否有元素
4. 如果没有,直接加入
5. 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后
6. 再Java8中,如果一条链表的元素个数超过`TREEIFY_THRESHOLD`(默认为8),并且table的大小大于等于`MIN_TREEIFY_CAPACITY`(默认为64)就会进行树化,否则仍然采用数组进行扩容
HashMap更详细的机制可以参考HashSet的扩容机制:
HashSet(HashMap)底层原码剖析详解 https://blog.csdn.net/m0_54068390/article/details/137056523
- 适用场景
-
当需要高效地存储和检索键值对,并且不需要保证顺序时,
HashMap
是一个不错的选择。 -
适用于需要快速查找、插入和删除键值对的场景。
HashMap
是 Java 中使用广泛的数据结构之一
3. LinkedHashMap
LinkedHashMap 是 Java 中的一个特殊的 Map 实现,它继承自 HashMap 类,同时使用链表维护插入顺序或者访问顺序(LRU 缓存淘汰算法)。LinkedHashMap 在 HashMap 的基础上增加了对顺序的维护,因此可以用于需要有序存储键值对的场景。
LinkedHashMap 的主要特点
- 有序性:LinkedHashMap 可以维护插入顺序或者访问顺序。
- 基于哈希表和链表:LinkedHashMap 内部维护了一个双向链表,该链表按照键值对的插入顺序或者访问顺序进行排序,同时使用哈希表来提供快速的键值对查找。
- 线程不安全:LinkedHashMap 不是线程安全的,如果多个线程同时访问 LinkedHashMap,需要在外部进行同步操作。
- 允许空键和空值:LinkedHashMap 允许键和值都为空,但是只能有一个空键。
LinkedHashMap的主要方法
LinkedHashMap 的构造方法
LinkedHashMap()
:创建一个空的 LinkedHashMap 实例,默认初始化容量为 16,加载因子为 0.75。LinkedHashMap(int initialCapacity)
:创建一个指定初始容量的 LinkedHashMap 实例。LinkedHashMap(int initialCapacity, float loadFactor)
:创建一个指定初始容量和加载因子的 LinkedHashMap 实例。LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
:创建一个指定初始容量、加载因子和顺序的 LinkedHashMap 实例,如果 accessOrder 参数为 true,则按访问顺序排序,否则按插入顺序排序。LinkedHashMap(Map<? extends K,? extends V> m)
:创建一个包含指定 Map 中所有键值对的 LinkedHashMap 实例。
LinkedHashMap 的常用方法
LinkedHashMap 继承自 HashMap,因此大部分方法和 HashMap 相同。此外,LinkedHashMap 还提供了一些方法用于操作顺序:
put(K key, V value)
:将指定的键值对插入到 LinkedHashMap 中。get(Object key)
:获取指定键对应的值。remove(Object key)
:移除指定键及其对应的值。containsKey(Object key)
:判断 LinkedHashMap 中是否包含指定的键。containsValue(Object value)
:判断 LinkedHashMap 中是否包含指定的值。size()
:返回 LinkedHashMap 中键值对的数量。isEmpty()
:判断 LinkedHashMap 是否为空。clear()
:清空 LinkedHashMap 中的所有键值对。keySet()
:返回一个包含 LinkedHashMap 中所有键的 Set 集合。values()
:返回一个包含 LinkedHashMap 中所有值的 Collection 集合。entrySet()
:返回一个包含 LinkedHashMap 中所有键值对的 Set 集合。firstKey()
:返回 LinkedHashMap 中的第一个键。lastKey()
:返回 LinkedHashMap 中的最后一个键。removeEldestEntry(Map.Entry<K,V> eldest)
:用于自定义删除最老元素的策略,如果需要实现 LRU 缓存淘汰算法,可以通过继承 LinkedHashMap 并覆写此方法来实现。
📌
在实际应用中,LinkedHashMap 通常用于需要按照插入顺序或者访问顺序存储键值对的场景,比如 LRU 缓存、LRU 页面置换算法等。
4. Hashtable
Hashtable
是 Java 中的一个经典数据结构,它实现了 Map
接口,并且是同步的,这意味着它的操作是线程安全的。它在 Java 早期版本中是常用的数据结构之一,但是随着 Java 集合框架的发展,Hashtable
逐渐被更强大的 HashMap
替代。然而,Hashtable
仍然存在,并且在某些情况下仍然有其用武之地。
Hashtable的主要特点
- 线程安全性:
Hashtable
是线程安全的,这意味着多个线程可以同时访问Hashtable
实例而不会导致数据不一致。 - 同步性:
Hashtable
中的方法都是同步的,这意味着在多线程环境下,每次只有一个线程可以修改Hashtable
中的内容,从而保证了线程安全性。 - 键值对存储:
Hashtable
存储的是键值对(key-value pairs),其中每个键都是唯一的,但值可以重复。 - 不允许键或值为null: 在
Hashtable
中,键和值都不允许为 null。如果试图将 null 作为键或值传递给Hashtable
的方法,则会抛出NullPointerException
。
hashtable的扩容机制
在 Java 中,Hashtable
的默认初始容量为 11,加载因子(load factor)为 0.75。
扩容操作会重新计算 Hashtable
的容量,并将存储的元素重新分布到新的更大的数组中。通常,新数组的容量会是原来容量的两倍加一(2 * oldCapacity + 1),这样做的目的是为了保持新容量是一个素数,这有助于减少哈希冲突的概率,从而提高性能。
当 Hashtable
扩容时,它会执行以下步骤:
- 创建一个新的数组,其容量是原来容量的两倍加一,并且容量是一个素数。
- 将
Hashtable
中的所有元素重新计算哈希值,并将它们分配到新数组中的合适位置。 - 更新
Hashtable
的内部状态,包括容量和阈值。
5. Properties
Properties
类是 Hashtable
的一个子类,它专门用于处理属性文件(.properties 文件),这些文件通常用于存储配置信息,比如应用程序的设置参数等。Properties
类提供了一种方便的方式来读取和写入这些属性文件,并且它保持了 Hashtable
的特性,例如键值对存储和线程安全性。
Properties的主要特点
- 键值对存储:
Properties
类存储的是键值对(key-value pairs),其中每个键和值都是字符串类型。与Hashtable
类似,键是唯一的,但是值可以重复。 - 读写属性文件:
Properties
类提供了从属性文件读取数据和将数据写入属性文件的方法,这使得它特别适合于处理配置信息。属性文件是以.properties
为扩展名的文本文件,其中每行包含一个键值对,以键和值之间的等号分隔。 - 默认值:
Properties
对象可以指定默认值,这些默认值在属性文件中找不到对应键时会被返回。
使用方法
- 加载属性文件: 使用
load(InputStream inStream)
方法从输入流中加载属性文件的内容。
Properties props = new Properties();
try (InputStream inputStream = new FileInputStream("config.properties")) {
props.load(inputStream);
}
- 获取属性值: 使用
getProperty(String key)
方法根据键获取对应的值。
String value = props.getProperty("key");
- 设置属性值: 使用
setProperty(String key, String value)
方法设置键值对。
props.setProperty("key", "value");
- 存储属性文件: 使用
store(OutputStream out, String comments)
方法将属性保存到输出流中,通常用于将更改后的属性写回到属性文件中。
try (OutputStream outputStream = new FileOutputStream("config.properties")) {
props.store(outputStream, "Updated properties");
}
6. TreeMap
TreeMap 是 Java 中的一个有序的键值对集合,它基于红黑树(Red-Black Tree)实现。TreeMap 继承自 AbstractMap 类并实现了 NavigableMap 接口。它提供了基于键的排序,以及对键值对集合的高效操作,比如插入、删除和查找。
TreeMap 的主要特点
- 有序性:TreeMap 中的键值对按照键的自然顺序或者通过 Comparator 进行排序。
- 基于红黑树:TreeMap 的内部实现是红黑树,这使得插入、删除和查找的时间复杂度为 O(log n)。
- 允许空键:TreeMap 中允许键为空,但是这样的键只能有一个。
- 不允许空值:TreeMap 不允许值为空,如果尝试插入空值会抛出 NullPointerException 异常。
- 线程不安全:TreeMap 不是线程安全的,如果多个线程同时访问 TreeMap,需要在外部进行同步操作
TreeMap 的主要方法
put(K key, V value)
:将指定的键值对插入到 TreeMap 中。get(Object key)
:获取指定键对应的值。r
****emove(Object key)
:移除指定键及其对应的值。containsKey(Object key)
:判断 TreeMap 中是否包含指定的键。containsValue(Object value)
:判断 TreeMap 中是否包含指定的值。size()
:返回 TreeMap 中键值对的数量。isEmpty()
:判断 TreeMap 是否为空。clear()
:清空 TreeMap 中的所有键值对。keySet()
:返回一个包含 TreeMap 中所有键的 Set 集合。values()
:返回一个包含 TreeMap 中所有值的 Collection 集合。entrySet()
:返回一个包含 TreeMap 中所有键值对的 Set 集合。firstKey()
:返回 TreeMap 中的第一个键。lastKey()
:返回 TreeMap 中的最后一个键。lowerKey(K key)
:返回小于指定键的最大键。higherKey(K key)
:返回大于指定键的最小键。
📌
在实际应用中,TreeMap 通常用于需要按照键的顺序进行存储和检索的场景,比如字典、排行榜等
17.4 集合的选择
Collection和Map的选择
-
Collection
- 如果你需要存储一组对象,并且不需要键值对的映射关系,应该选择Collection接口的实现类,如List、Set或Queue。
- 使用List接口的实现类(如ArrayList或LinkedList),如果需要按照元素的索引进行访问和操作。
- 使用Set接口的实现类(如HashSet或TreeSet),如果需要确保集合中元素的唯一性。
- 使用Queue接口的实现类(如ArrayDeque或PriorityQueue),如果需要实现队列的数据结构,通常用于实现FIFO(先进先出)的数据结构。
-
Map
-
如果你需要存储键值对的映射关系,并且需要通过键快速查找对应的值,应该选择Map接口的实现类,如HashMap、TreeMap或LinkedHashMap。
-
使用HashMap,如果不需要按照键的顺序进行遍历,并且对快速查找键值对有较高的需求。
-
使用TreeMap,如果需要按照键的自然顺序或者自定义顺序进行遍历,并且对有序遍历键值对有较高的需求。
-
使用LinkedHashMap,如果需要保持插入顺序或者访问顺序,并且对保持元素顺序有较高的需求。
-
在Java实际开发中,选择使用哪种集合取决于具体需求和数据结构。下面是一些常见的情况:
- ArrayList vs. LinkedList
- 如果需要频繁随机访问元素,应该选择ArrayList,因为它的随机访问时间复杂度是 O(1)。
- 如果需要频繁地在列表中插入或删除元素,应该选择LinkedList,因为它的插入和删除操作的时间复杂度是 O(1)。
- HashMap vs. TreeMap vs. LinkedHashMap
- 如果需要快速的查找、插入和删除键值对,并且不需要按照键的顺序进行遍历,应该选择HashMap。它的时间复杂度是 O(1)。
- 如果需要按照键的自然顺序或者自定义顺序进行遍历,应该选择TreeMap。它会根据键的顺序维护一棵红黑树,插入、删除和查找的时间复杂度是 O(log n)。
- 如果需要维护插入顺序或者访问顺序,应该选择LinkedHashMap。
- HashSet vs. TreeSet vs. LinkedHashSet
- 如果需要快速的查找、插入和删除元素,并且不需要元素有序,应该选择HashSet。它的时间复杂度是 O(1)。
- 如果需要按照元素的自然顺序或者自定义顺序进行遍历,应该选择TreeSet。它会根据元素的顺序维护一棵红黑树,插入、删除和查找的时间复杂度是 O(log n)。
- 如果需要维护插入顺序或者访问顺序,应该选择LinkedHashSet。
- Stack vs. LinkedList
- 如果只需要栈的基本操作(压栈和弹栈),可以使用Stack类。
- 如果需要更多灵活性或者性能更好,也可以使用LinkedList实现栈。
- Queue和Deque
- 如果需要实现先进先出(FIFO)的队列,应该选择LinkedList或者ArrayDeque。
- 如果需要实现先进先出和后进先出(LIFO)的队列,应该选择LinkedList或者ArrayDeque,然后根据需要使用push和pop操作。
17.5 Collections工具类
java.util.Collections
是 Java 中的一个实用工具类,提供了一系列静态方法,用于对集合进行操作、排序、查找、同步等。这些方法大大简化了集合操作的过程,提高了开发效率。
主要功能
- 集合操作:Collections 类提供了一系列方法来操作集合,比如添加元素、移除元素、替换元素等。
- 排序:Collections 类提供了用于对 List 集合进行排序的方法,包括自然排序和自定义排序。
- 查找:Collections 类提供了用于在集合中查找指定元素的方法。
- 同步:Collections 类提供了一系列同步方法,可以将非线程安全的集合转换为线程安全的集合。
- 包装:Collections 类提供了一系列静态方法来创建不可变的集合、单元素集合等。
常用方法
- 操作方法
addAll(Collection<? super T> c, T... elements)
:向指定集合中添加指定的元素。replaceAll(List<T> list, T oldVal, T newVal)
:将列表中所有的 oldVal 替换为 newVal。reverse(List<?> list)
:反转列表中的元素顺序。shuffle(List<?> list)
:随机重排列表中的元素。swap(List<?> list, int i, int j)
:交换列表中指定位置的元素。
- 排序方法
sort(List<T> list)
:对列表中的元素按照自然顺序进行排序。sort(List<T> list, Comparator<? super T> c)
:使用指定的比较器对列表中的元素进行排序。binarySearch(List<? extends Comparable<? super T>> list, T key)
:在有序列表中使用二分查找查找指定元素。binarySearch(List<? extends T> list, T key, Comparator<? super T> c)
:在有序列表中使用指定的比较器进行二分查找。
- 查找方法
frequency(Collection<?> c, Object o)
:返回指定集合中指定元素出现的次数。indexOfSubList(List<?> source, List<?> target)
:返回源列表中第一次出现与目标列表相同的子列表的起始位置。lastIndexOfSubList(List<?> source, List<?> target)
:返回源列表中最后一次出现与目标列表相同的子列表的起始位置。
- 同步方法
synchronizedCollection(Collection<T> c)
:返回一个同步的(线程安全的)集合。synchronizedList(List<T> list)
:返回一个同步的(线程安全的)列表。synchronizedMap(Map<K,V> m)
:返回一个同步的(线程安全的)映射。synchronizedSet(Set<T> s)
:返回一个同步的(线程安全的)集合。synchronizedSortedMap(SortedMap<K,V> m)
:返回一个同步的(线程安全的)有序映射。synchronizedSortedSet(SortedSet<T> s)
:返回一个同步的(线程安全的)有序集合。
- 包装方法
emptyList()
:返回一个空的、不可修改的列表。emptySet()
:返回一个空的、不可修改的集合。emptyMap()
:返回一个空的、不可修改的映射。singleton(T o)
:返回一个包含指定元素的不可修改的列表。singletonList(T o)
:返回一个包含指定元素的不可修改的列表。singletonMap(K key, V value)
:返回一个包含指定键值对的不可修改的映射。
📌
在实际应用中,Collections 类的方法为集合操作提供了便利,能够大大简化开发过程。
十八、Java泛型
18.1 泛型介绍
泛型(Generics)是 Java 编程语言的一个重要特性,它允许在编写代码时使用参数化类型。泛型的引入使得程序可以更加灵活地处理不同类型的数据,提高了代码的可读性、安全性和可维护性。
为什么需要泛型?
在早期的 Java 中,集合类(如 ArrayList
、LinkedList
等)只能存储 Object
类型的对象。这就导致了一些问题:
- 类型安全问题: 编译器无法在编译时检查代码是否正确地使用了类型。因为所有类型都被视为
Object
,所以编译器无法捕捉到错误的类型转换。 - **代码重复:**如果要编写能够处理不同类型的集合类,就需要为每种类型编写一个新的类,导致了大量的重复代码。
泛型的出现解决了上述问题,它允许我们在编写类、接口和方法时使用类型参数,使得代码可以更好地处理不同类型的数据。
泛型的基本语法
在 Java 中,使用尖括号 < >
来指定泛型类型,例如:
class MyClass<T> {
// 类体中可以使用类型参数 T
}
这里的 T
是一个类型参数,可以是任何合法的标识符。在实际使用时,可以用具体的类型来替换 T
,比如 Integer
、String
等。
注意事项
- 泛型类型只能是引用类型
- 在给泛型指定具体类型后,可以传递该类型,或子类类型
- 在实际开发中往往简写,使用泛型时将<>内的类型省略
new ArrayList<>()
18.2 自定义泛型
泛型类
泛型类是使用泛型类型参数的类。定义泛型类的基本语法已经在上面示例中展示了。
细节:
- 普通成员可以使用泛型(属性和方法等)
- 使用泛型的数组,不能初始化(数组在new时不能确定T类型,就无法在内存开空间)
- 静态方法中不能使用类的泛型
- 泛型类的类型,是在创建对象时确定的(因为创建对象时,需要指定确定类型)
- 如果在创建对象时,没有指定类型,默认为Object
以下是一个更具体的示例:
class Box<T> {
private T data;
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
在这个例子中,Box
类可以存储任何类型的数据。
泛型接口
细节:
- 接口中,静态成员也不能使用泛型(和泛型类规定相同,接口中的成员)
- 泛型接口的类型,在继承接口或者实现接口时确定
- 没有指定类型,默认为Object
public interface Container<T> {
// 添加元素到容器
void add(T item);
// 从容器中获取指定索引处的元素
T get(int index);
// 获取容器中元素的数量
int size();
}
泛型方法
除了类之外,Java 还支持泛型方法。泛型方法是在调用时可以接受不同类型参数的方法。
细节:
- 泛型方法,可以定义在普通类中,也可以定义在泛型类中
- 泛型方法被调用时,类型就确定了
- 下面是一个简单的示例:
public <T> T myMethod(T value) {
// 方法体中可以使用类型参数 T
return value;
}
18.3 泛型继承和通配符
泛型继承
泛型没有继承性
通配符
有时候,我们可能不关心泛型的具体类型,只关心它是某种类型或其子类型。这时候可以使用通配符 ?
。例如:
List<?> list = new ArrayList<>();
List<? extends A>
List<? super B>
这里的 list
可以引用任何类型的 List
,但是我们无法向其添加任何元素,因为我们不知道 List
中存储的元素类型是什么。
类型擦除
Java 的泛型是通过类型擦除实现的。这意味着泛型类型信息在编译时会被擦除,编译器会把泛型类型转换为原始类型(raw type)。这样做是为了向后兼容以前的版本,并允许泛型代码与旧代码一起使用。
十九、多线程
19.1 线程(Thread)介绍
进程和线程
什么是进程?
进程是计算机中运行的程序的实例。它是操作系统分配资源(如CPU时间、内存空间)的基本单位。
什么是线程?
- 线程是程序执行的单元,是CPU调度和执行的基本单位。
- 在Java中,线程是Thread类的对象,可以通过继承Thread类或实现Runnable接口来创建线程。
单线程和多线程
单线程:单线程指的是程序在执行过程中只有一个主线程在运行,所有的任务都在一个线程中顺序执行。
多线程:多线程指的是程序在执行过程中同时存在多个线程,每个线程可以独立执行不同的任务。
并发和并行
并发:并发是指多个任务在同一时间段内交替执行,通过时间片轮转等机制实现任务的快速切换,给人以同时执行的错觉。
并行:并行是指多个任务在同一时刻同时执行,利用多核处理器或分布式系统的资源实现真正的同时执行。
19.2 线程使用
创建线程的两种方法
继承Thread
通过继承Thread类,可以创建一个新的线程类,并重写其run()方法来定义线程执行的任务。
class MyThread extends Thread {
public void run() {
// 线程执行的任务代码
System.out.println("线程执行任务...");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread(); // 创建线程对象
thread.start(); // 启动线程
}
}
特点:直接继承Thread类,简单方便;适用于简单的线程任务。
为什么要调用start()方法
在Java中,多线程要调用start()方法而不是直接调用run()方法的原因是因为:
- 启动新线程:调用start()方法会告诉操作系统创建一个新的线程,并在新线程中执行指定的任务(run()方法)。如果直接调用run()方法,那么任务将在当前线程中执行,而不会创建新的线程,因此无法实现并发执行的效果。
- 线程调度:调用start()方法会使新线程进入就绪状态,等待操作系统的调度分配CPU资源。操作系统会根据线程的优先级等因素来调度各个线程,实现合理的并发执行。如果直接调用run()方法,任务将在当前线程中同步执行,无法利用多核处理器等资源,效率会降低。
- 线程安全性:多线程环境下,直接调用run()方法可能会引发线程安全问题。因为多个线程可能会同时调用同一个对象的run()方法,导致数据竞争和不一致的结果。而通过调用start()方法,可以确保每个线程都在自己的执行环境中运行,提高了线程的安全性。
实现Runnable
java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时再用继承Thread类方法创建线程就不可能了,所以就提供了Runnable接口来创建线程。
通过实现Runnable接口,可以将线程的任务从线程类中分离出来,使得多个线程可以共享相同的任务。
class MyRunnable implements Runnable {
public void run() {
// 线程执行的任务代码
System.out.println("线程执行任务...");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable(); // 创建Runnable对象
Thread thread = new Thread(myRunnable); // 创建线程对象,并传入Runnable对象
thread.start(); // 启动线程
}
}
特点:
- 通过实现Runnable接口,实现了任务与线程的分离,提高了代码的可复用性。
- 可以实现多个线程共享同一个任务,适用于多线程并发执行相同任务的情况。
- 相比继承Thread类,更灵活,更符合面向对象的设计原则。
两种创建线程方式的选择 - 如果只是简单的任务,可以直接继承Thread类。
- 如果需要将任务和线程分离,或者多个线程共享同一个任务,建议实现Runnable接口。
继承Thread和实现Runnable的区别
从二者实现方式来看,本质没有区别,实现Runnable接口方式更适合多个线程共享一个资源的情况,并且避免了单继承的限制,推荐使用实现Runnable接口的方式
19.3 多线程售票问题
多线程售票问题是一个经典的并发编程问题,通常用于说明多线程编程中的竞态条件(Race Condition)和线程安全性问题。问题描述如下:
假设有一个电影院,共有100张电影票,现在有3个售票窗口,每个窗口的售票速度不一样,可能会出现多个售票窗口同时售出同一张票或者售出超过剩余票数的情况。我们需要设计一个多线程程序来模拟这个售票过程,并确保售票的线程安全性,即不会出现超卖或重复售票的情况。
//这段代码就会出现超卖和重复售票的情况
public class TicketCounter {
private int tickets = 100;
public void sellTicket() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出第" + tickets + "张票");
tickets--;
} else {
System.out.println("票已售罄");
}
}
public static void main(String[] args) {
TicketCounter ticketCounter = new TicketCounter();
Thread window1 = new Thread(() -> {
while (ticketCounter.tickets > 0) {
ticketCounter.sellTicket();
}
}, "窗口1");
Thread window2 = new Thread(() -> {
while (ticketCounter.tickets > 0) {
ticketCounter.sellTicket();
}
}, "窗口2");
Thread window3 = new Thread(() -> {
while (ticketCounter.tickets > 0) {
ticketCounter.sellTicket();
}
}, "窗口3");
window1.start();
window2.start();
window3.start();
}
}
解决方案:
一种简单的解决方案是使用同步机制来保证线程安全性。可以使用 synchronized 关键字或者 Lock 接口来实现同步。以下是一个使用 synchronized 关键字的示例:
public class TicketCounter {
private int tickets = 100;
public synchronized void sellTicket() {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出第" + tickets + "张票");
tickets--;
} else {
System.out.println("票已售罄");
}
}
public static void main(String[] args) {
TicketCounter ticketCounter = new TicketCounter();
Thread window1 = new Thread(() -> {
while (ticketCounter.tickets > 0) {
ticketCounter.sellTicket();
}
}, "窗口1");
Thread window2 = new Thread(() -> {
while (ticketCounter.tickets > 0) {
ticketCounter.sellTicket();
}
}, "窗口2");
Thread window3 = new Thread(() -> {
while (ticketCounter.tickets > 0) {
ticketCounter.sellTicket();
}
}, "窗口3");
window1.start();
window2.start();
window3.start();
}
}
19.4 线程终止和中断
通知线程终止
要通知线程终止,可以使用一个标志变量来控制线程的执行状态。线程在执行过程中不断检查这个标志变量,当标志变量达到终止条件时,线程自行结束执行。
以下是一个示例代码:
public class MyThread extends Thread {
private volatile boolean running = true; // 标志变量,控制线程执行状态
public void stopThread() {
running = false; // 设置标志变量为false,通知线程终止
}
@Override
public void run() {
while (running) {
// 线程执行的任务代码
System.out.println("Thread is running...");
try {
Thread.sleep(1000); // 休眠1秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread is terminated.");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
// 主线程休眠5秒钟后通知线程终止
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.stopThread(); // 通知线程终止
}
}
在上面的代码中,线程通过不断检查 running
变量的值来确定是否继续执行任务。当 running
变量被设置为 false
时,线程终止执行。通过调用 stopThread()
方法可以设置 running
变量为 false
,从而通知线程终止。
线程中断
线程中断是一种线程间的通信机制,用于请求线程停止正在执行的任务。在Java中,线程中断通过调用线程对象的 interrupt()
方法来实现。当线程被中断时,它会收到一个中断信号,但是线程本身决定如何处理这个中断信号。
以下是关于线程中断的一些重要信息和用法:
- 中断状态
- 每个线程都有一个中断状态,用于标识是否收到了中断信号。
- 可以通过
Thread
类的isInterrupted()
方法检查线程的中断状态,或者通过Thread.interrupted()
方法检查当前线程的中断状态,并清除中断状态。
- 中断响应
- 线程可以通过捕获
InterruptedException
异常来响应中断。 - 线程在阻塞状态(如调用
Object.wait()
、Thread.sleep()
、Thread.join()
等方法时)下收到中断信号会抛出InterruptedException
异常。
- 线程可以通过捕获
- 清除中断状态
- 如果线程捕获了
InterruptedException
异常,可以选择重新设置中断状态(使用Thread.currentThread().interrupt()
)或者不处理中断状态。 - 如果线程决定不处理中断状态,可以使用
Thread.interrupted()
方法来清除中断状态,以避免后续代码误认为线程已经被中断。
- 如果线程捕获了
- 线程中断的建议
- 在设计多线程应用时,要考虑到线程中断的处理机制,合理设计线程的中断逻辑。
- 在执行阻塞操作的代码块中,要注意捕获
InterruptedException
异常,并及时处理中断。
以下是一个简单的示例代码,演示了线程的中断操作:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Thread is running...");
Thread.sleep(1000); // 模拟耗时操作
}
} catch (InterruptedException e) {
// 线程收到中断信号,处理中断异常
System.out.println("Thread is interrupted.");
}
});
thread.start(); // 启动线程
// 主线程休眠3秒钟后中断线程
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt(); // 中断线程
}
}
19.5 线程方法
Java线程类提供了一些常用的方法,用于管理线程的状态、执行、中断等操作。以下是一些常用的Java线程方法:
- **start():**启动线程。调用该方法后,线程的
run()
方法会在一个新的线程中执行。 - **run():**线程的执行逻辑。在
Thread
类中,该方法是一个空方法,需要子类重写以定义线程的执行任务。 - **sleep(long millis):**使当前线程休眠指定的毫秒数。线程在休眠期间不会占用CPU资源,可以让出CPU给其他线程。
- **join()**等待该线程终止。调用该方法会使当前线程等待调用线程对象的终止,直到线程终止才会继续执行。
- **interrupt():**中断线程。调用该方法会给目标线程发送一个中断信号,线程收到中断信号后可以做出相应的处理。
- **isInterrupted():**判断线程是否被中断。该方法返回线程的中断状态,但不会清除中断状态。
- **interrupted():**判断当前线程是否被中断,并清除中断状态。该方法返回当前线程的中断状态,并清除中断状态,使得后续的调用返回 false。
- **yield():**让出CPU执行时间,让同优先级的线程有机会执行。调用该方法后,当前线程会放弃CPU资源,进入就绪状态。(礼让的时间不确定,不一定礼让成功)
- **setName(String name):**设置线程的名称。
- **getName():**获取线程的名称。
- **isAlive():**判断线程是否存活。如果线程已经启动且尚未终止,则返回 true。
- **setDaemon(boolean on):**将线程设置为守护线程。守护线程是一种特殊的线程,当所有的非守护线程结束时,守护线程会自动退出。
- **getState():**获取线程的状态。返回一个
Thread.State
枚举值,表示线程的当前状态,如 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。
19.6 守护线程
Java中的守护线程(Daemon Thread)是一种特殊类型的线程,其生命周期依赖于其他非守护线程的结束。当所有的非守护线程结束时,JVM会自动退出,并且会强制终止所有守护线程,即使它们尚未执行完毕。
特点:
- 生命周期依赖:守护线程的生命周期依赖于其他非守护线程。当所有非守护线程结束时,JVM会自动退出,此时守护线程也会被强制终止。
- 后台执行:守护线程通常用于执行后台任务,不影响应用程序的正常运行。当所有非守护线程结束时,JVM会自动退出,而不必等待守护线程执行完毕。
- 线程优先级:与普通线程一样,守护线程也有优先级,但是它们的优先级通常比较低。在默认情况下,守护线程的优先级与创建它的父线程相同。
使用场景
- 后台任务:守护线程通常用于执行一些不需要等待或者无法在应用程序关闭前完成的后台任务,如垃圾回收、日志记录、定时任务等。
- 资源管理:守护线程可以用于管理资源,如监控文件系统、网络连接等,及时释放资源以避免资源泄露。
创建守护线程
在Java中,可以通过设置线程的 setDaemon(true)
方法将线程设置为守护线程。以下是一个示例:
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("Daemon Thread is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemonThread.setDaemon(true); // 将线程设置为守护线程
daemonThread.start();
System.out.println("Main Thread is finished.");
}
}
在这个示例中,daemonThread
被设置为守护线程。当主线程执行完毕并退出时,守护线程也会被自动终止,而不需要等待守护线程执行完毕。
需要注意的是,守护线程通常用于执行一些后台任务,因此要确保它们不会阻塞主线程或其他重要线程。因为一旦所有非守护线程结束,守护线程会被强制终止,可能会导致未完成的任务被中断或丢失。
19.7 线程生命周期
Java中的线程生命周期描述了一个线程从创建到结束的整个过程,包括线程的各种状态和状态之间的转换。Java线程的生命周期包括以下状态:
Java线程的生命周期包括以下状态:
-
NEW(新建状态)
- 当线程对象被创建但还没有调用
start()
方法时,线程处于新建状态。 - 此时线程对象已经被创建,但尚未分配系统资源,不具备执行条件。
- 当线程对象被创建但还没有调用
-
RUNNABLE(就绪状态)
- 当线程对象调用
start()
方法后,线程进入就绪状态。 - 处于就绪状态的线程已经分配了系统资源,并等待系统调度来执行。
- 当线程对象调用
-
RUNNING(运行状态)
- 当线程获得CPU资源并开始执行线程的
run()
方法时,线程进入运行状态。 - 处于运行状态的线程正在执行其任务代码。
- 当线程获得CPU资源并开始执行线程的
-
BLOCKED(阻塞状态)
- 当线程试图进入同步块时,如果同步块已经被其他线程占用,线程将进入阻塞状态。
- 处于阻塞状态的线程暂时停止执行,并等待获取锁。
-
WAITING(等待状态)
- 当线程调用
Object.wait()
、Thread.join()
、LockSupport.park()
等方法时,线程进入等待状态。 - 处于等待状态的线程会一直等待,直到被其他线程唤醒。
- 当线程调用
-
TIMED_WAITING(计时等待状态)
- 当线程调用
Thread.sleep()
、Object.wait(long)
、Thread.join(long)
、LockSupport.parkNanos()
等带有超时参数的方法时,线程进入计时等待状态。 - 处于计时等待状态的线程会等待一定时间,或者直到被其他线程唤醒。
- 当线程调用
-
TERMINATED(终止状态)
- 线程的
run()
方法执行完毕,或者线程因异常退出,线程进入终止状态。 - 处于终止状态的线程已经完成了其任务,不再执行任何代码。
- 线程的
线程在不同的状态之间会发生转换,具体的状态转换如下:
- 新建状态(NEW) -> 就绪状态(RUNNABLE):调用
start()
方法。 - 就绪状态(RUNNABLE) -> 运行状态(RUNNING):获取CPU资源并开始执行任务。
- 运行状态(RUNNING) -> 就绪状态(RUNNABLE):调用
yield()
方法,暂时让出CPU资源。 - 运行状态(RUNNING) -> 阻塞状态(BLOCKED):试图进入同步块,但同步块已被其他线程占用。
- 运行状态(RUNNING) -> 终止状态(TERMINATED):
run()
方法执行完毕,或者线程因异常退出。 - 等待状态(WAITING) -> 就绪状态(RUNNABLE):被其他线程唤醒,或者等待超时。
- 计时等待状态(TIMED_WAITING) -> 就绪状态(RUNNABLE):等待时间到达,或者被其他线程唤醒。
19.8 线程同步机制
Java的线程同步机制是为了解决多线程并发访问共享资源时可能出现的竞态条件(Race Condition)和数据不一致性的问题。在多线程环境下,如果多个线程同时访问共享资源,并且其中一个线程修改了资源的状态,而其他线程可能正在读取或修改相同的资源,就会导致数据不一致性的问题。
Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。
synchronized关键字
synchronized关键字可以应用在方法上或代码块上,它确保了在同一时刻最多只有一个线程可以执行被synchronized关键字保护的代码。当一个线程进入synchronized代码块时,会尝试获取对象的锁,如果获取成功,则执行代码块中的代码,执行完毕后释放锁,其他线程才能获取锁并执行代码块。
在方法上使用synchronized:
public synchronized void synchronizedMethod() {
// 同步的代码块
}
在代码块上使用synchronized:
public void someMethod() {
synchronized (this) {
// 同步的代码块
}
}
Lock接口
Lock接口提供了比synchronized更灵活的锁机制,它允许更复杂的锁获取和释放,并且支持更灵活的锁定模式。Lock接口的实现类包括ReentrantLock、ReentrantReadWriteLock等。
使用ReentrantLock:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private Lock lock = new ReentrantLock();
public void someMethod() {
lock.lock();
try {
// 同步的代码块
} finally {
lock.unlock();
}
}
}
同步块与同步方法的区别
- 粒度不同: 同步方法是对整个方法进行同步,而同步代码块可以控制需要同步的代码片段。
- 锁的对象不同: 同步方法的锁是当前对象实例(对于静态方法是Class对象),而同步代码块可以指定任意对象作为锁。
- 灵活性: 使用Lock接口可以提供更灵活的锁定方式,例如可以尝试获取锁并在超时后放弃等待。
线程同步的优势
- 确保数据一致性: 通过线程同步机制,可以确保多个线程访问共享资源时的数据一致性。
- 避免竞态条件: 线程同步可以避免竞态条件的发生,保证了程序的正确性。
- 提高程序安全性: 同步机制可以避免多线程环境下的数据混乱和不一致性,从而提高程序的安全性。
19.9 互斥锁
互斥锁(Mutex Lock)是一种常见的线程同步机制,用于确保在任意时刻只有一个线程可以访问共享资源。互斥锁的核心概念是,当一个线程获得了锁时,其他线程就无法获得锁,只能等待直到锁被释放。
互斥锁的特点
- **独占性:**一次只能有一个线程持有锁,其他线程无法同时持有同一个锁。
- 阻塞: 如果一个线程尝试获取锁,而锁已经被其他线程持有,那么该线程会被阻塞,直到获取到锁为止。
- 非递归性: 互斥锁通常是非递归的,即同一个线程在持有锁的情况下,再次请求该锁会导致线程阻塞。只有当锁被释放后,该线程才能再次获得锁。
互斥锁的使用方式
在Java中,互斥锁通常通过synchronized关键字或Lock接口来实现。
使用synchronized关键字:
public class Example {
private final Object lock = new Object();
public void synchronizedMethod() {
synchronized (lock) {
// 同步的代码块
}
}
}
使用Lock接口(ReentrantLock):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private final Lock lock = new ReentrantLock();
public void someMethod() {
lock.lock();
try {
// 同步的代码块
} finally {
lock.unlock();
}
}
}
互斥锁的优缺点
优点:
- 简单易用:互斥锁是一种常见的线程同步机制,使用起来比较简单。
- 功能强大:可以有效地避免多线程环境下的竞态条件和数据不一致性问题。
缺点:
- 开销较大:由于互斥锁需要在多线程之间进行频繁的锁竞争和状态切换,因此可能会带来一定的性能开销。
- 可能导致死锁:如果使用不当,可能会出现死锁情况,即多个线程相互等待对方释放锁而无法继续执行的情况。
19.10 死锁
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种僵局,导致线程无法继续执行下去。每个线程都在等待其他线程释放资源,而自己却不释放已经占有的资源,最终导致所有线程都无法继续执行。
死锁的产生条件
- **互斥条件(Mutual Exclusion):**至少有一个资源必须是被独占的,即同一时刻只能被一个线程占用。
- 请求与保持条件(Hold and Wait): 一个线程持有至少一个资源,并且正在等待获取其他线程持有的资源。
- **不可剥夺条件(No Preemption):**资源只能由持有它的线程释放,不能被其他线程强行抢占。
- **环路等待条件(Circular Wait):**存在一组等待资源的线程,每个线程都在等待下一个线程所持有的资源,形成一个循环等待的链。
只有当这四个条件同时满足时,死锁才会发生。
死锁的示例
假设有两个线程A和B,以及两个资源X和Y。线程A已经获取了资源X,并在等待资源Y,而线程B已经获取了资源Y,并在等待资源X。此时,线程A和B都无法继续执行下去,因为它们都在等待对方释放资源,这就形成了死锁。
如何避免死锁
- **避免使用多个锁:**尽量减少对多个资源的同时请求,可以尝试将多个资源合并成一个资源,或者重新设计程序逻辑以减少对多个资源的依赖。
- **按顺序获取锁:**对于多个资源,可以规定一个固定的获取顺序,并确保所有线程按照相同的顺序获取资源,避免出现循环等待的情况。
- **超时机制:**在获取资源时设置超时时间,如果超过一定时间仍然无法获取资源,则放弃并释放已经持有的资源,避免长时间等待。
- **资源分配图:**分析程序中的资源依赖关系,构建资源分配图,检测是否存在环路等待的情况,从而及时发现潜在的死锁问题。
- **死锁检测和恢复:**使用专门的工具或算法进行死锁检测,并在检测到死锁时采取相应的恢复措施,例如释放资源或终止部分线程。
释放锁
下面的操作会释放锁:
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到的break、return
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
下面的操作不会释放锁:
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,不会释放锁
二十、IO流
20.1 文件
文件概念
文件是计算机中用于存储数据的一种形式,通常用于永久性存储数据。文件可以是文本文件、图像文件、音频文件、视频文件等。在Java中,文件被抽象成File类,该类提供了对文件的创建、删除、重命名、查询等操作。
文件流
文件流是一种用于读取或写入文件数据的抽象概念。在Java中,文件流通常分为输入流和输出流,分别用于从文件读取数据和向文件写入数据。文件流提供了一种逐字节或逐块读写文件数据的方式。
文件对象构造器
在Java中,可以使用File类来代表文件或目录,并对其进行操作。File类提供了多个构造函数,用于创建File对象。下面是常用的File对象的构造器:
File(String pathname): 使用指定路径名字符串构造一个新的File实例,路径名可以是绝对路径或相对路径。
File file = new File("example.txt");
File(String parent, String child): 使用指定的父路径字符串和子路径字符串创建一个新的File实例。
File file = new File("C:/temp", "example.txt");
File(File parent, String child): 从父抽象路径名和子路径名字符串创建新的File实例。
File parentDir = new File("C:/temp");
File file = new File(parentDir, "example.txt");
File(URI uri): 通过将给定的文件或目录的路径名解析为抽象路径名来创建新的File实例。
File file = new File("file:///C:/temp/example.txt");
常用操作
创建文件: 使用File类的构造方法可以创建File对象,通过调用相关方法可以创建新文件。
File file = new File("example.txt");
if (file.createNewFile()) {
System.out.println("文件创建成功");
} else {
System.out.println("文件已存在");
}
删除文件: 使用File类的delete()方法可以删除指定的文件。
File file = new File("example.txt");
if (file.delete()) {
System.out.println("文件删除成功");
} else {
System.out.println("文件删除失败");
}
文件读取: 使用FileInputStream或FileReader类可以创建文件输入流,从文件中读取数据。
try (FileInputStream fis = new FileInputStream("example.txt")) {
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
文件写入: 使用FileOutputStream或FileWriter类可以创建文件输出流,向文件中写入数据。
try (FileOutputStream fos = new FileOutputStream("example.txt")) {
String data = "Hello, world!";
fos.write(data.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
常用方法
在Java中,File类提供了一系列常用的方法,用于操作文件和目录。下面列举了一些常用的File类方法:
创建文件或目录:
boolean createNewFile()
: 创建一个新文件。如果文件已存在,则返回false。
File file = new File("example.txt");
boolean created = file.createNewFile();
删除文件或目录:
boolean delete()
: 删除文件或目录。如果成功删除,则返回true。
File file = new File("example.txt");
boolean deleted = file.delete();
文件信息获取:
String getName()
: 返回文件或目录的名称。String getPath()
: 返回文件或目录的路径。boolean exists()
: 判断文件或目录是否存在。boolean isFile()
: 判断是否为文件。boolean isDirectory()
: 判断是否为目录。long lastModified()
: 返回文件或目录的最后修改时间。String getAbsolutePath()
: 获取文件绝对路径。String getParent()
:获取文件父级目录。long length()
:获取文件大小(字节)。
File file = new File("example.txt");
String name = file.getName();
boolean isFile = file.isFile();
文件操作:
boolean renameTo(File dest)
: 重命名文件或目录。long length()
: 返回文件的长度(字节数)。String[] list()
: 返回目录下的所有文件和目录的名称。File[] listFiles()
: 返回目录下的所有文件和目录的File对象数组。
File file = new File("example.txt");
boolean renamed = file.renameTo(new File("new_example.txt"));
目录操作:
boolean mkdir()
: 创建一个一级目录。如果目录已存在,则返回false。boolean mkdirs()
: 创建一个多级目录,包括所有必需但不存在的父目录。File getParentFile()
: 返回父目录的File对象。File[] listFiles(FileFilter filter)
: 返回目录下满足过滤条件的所有文件和目录的File对象数组。
File file = new File("example.txt");
File parentDir = file.getParentFile();
20.2 IO流原理及流的分类
IO流原理
I/O是Input/Output的缩写,I/O技术是非常实用的技术,用于处理数据传输,如读写文件,网络通讯等。
Java程序中,对于数据的输入输出操作以”流“的方式进行
java.io包下提供了各种流类和接口,用以获取不同种类的数据,并通过方法输入输出数据
I/O流分为字节流和字符流两种类型。
I/O流的工作原理
流的分类:
-
字节流和字符流:
- 字节流以字节为单位进行读写操作,适用于二进制数据(如图像、音频等)。
- 字符流以字符为单位进行读写操作,适用于文本数据(字符编码为Unicode)。
-
输入流和输出流:
- 输入流用于从外部源(如文件、网络连接、键盘等)读取数据到程序中。
- 输出流用于将程序中的数据写入到外部目标(如文件、网络连接、屏幕等)。
-
字节流和字符流之间的转换:
- 字符流可以通过字节流来实现,通过指定字符编码(如UTF-8)将字符转换为字节进行读写操作。
- 字节流也可以通过字符流来实现,通过指定字符编码将字节转换为字符进行读写操作。
-
阻塞和非阻塞:
- 阻塞I/O流会在读取或写入数据时阻塞当前线程,直到操作完成或超时。
- 非阻塞I/O流会立即返回,无论是否读取或写入数据成功,程序可以继续执行其他操作。
-
字节流(Byte Streams):
- 字节输出流:OutputStream及其子类,用于将字节数据写入到输出目标中。
- 字节输入流:InputStream及其子类,用于从输入源中读取字节数据。
-
字符流(Character Streams):
- 字符输入流:Reader及其子类,用于从输入源中读取字符数据。
- 字符输出流:Writer及其子类,用于将字符数据写入到输出目标中。
-
节点流和处理流:
- 节点流直接连接到输入源或输出目标,可以读取或写入原始数据。
- 处理流包装在节点流之上,提供额外的功能,如缓冲、转换等,提高了性能和灵活性。
-
对象流(Object Streams):
- 用于读写Java对象的序列化数据,可以将对象序列化为字节流并存储在文件中,或将字节流反序列化为对象。
📌
OutputStream、InputStream、Reader、Writer四个类都是抽象类,由这四个类派生出来的子类名称都是以其父类名作为子类名后缀的。
20.3 节点流和处理流
节点流(原始流)
- 作用: 节点流直接与数据源(如文件、内存、网络连接等)相连,用于直接读写数据。
- 特点: 直接操作底层数据源,提供了最基本的读写功能,属于低级别的流。
- 示例:
FileInputStream
、FileOutputStream
、FileReader
、FileWriter
等。 - 应用场景: 适用于直接对文件或其他数据源进行读写操作,效率高,但功能相对简单。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class NodeStreamExample {
public static void main(String[] args) {
String sourceFile = "source.txt";
String targetFile = "target.txt";
try {
// 使用节点流 FileInputStream 读取文件内容
FileInputStream inputStream = new FileInputStream(sourceFile);
// 使用节点流 FileOutputStream 将读取的内容写入到新文件
FileOutputStream outputStream = new FileOutputStream(targetFile);
int data;
while ((data = inputStream.read()) != -1) {
outputStream.write(data);
}
inputStream.close();
outputStream.close();
System.out.println("File copied successfully!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
处理流(包装流)
- 作用: 处理流不直接与数据源相连,而是包装在节点流之上,用于提供额外的功能或简化操作。
- 特点: 提供了一系列高级别的功能,如缓冲、转换、过滤等,屏蔽了底层数据源的细节,提供更方便的接口。
- 示例:
BufferedInputStream
、BufferedOutputStream
、BufferedReader
、BufferedWriter
等。 - 应用场景: 适用于对数据进行高级别的处理,如缓冲、编码转换、数据过滤等,提高了程序的效率和易用性。
工作原理 - 当使用处理流时,它会包装一个节点流,形成一个流的链条,数据在流的链条上依次流动。
- 处理流在数据传输过程中提供了额外的功能,比如缓冲、编码转换等。
- 处理流通过调用节点流的方法来实现功能,对程序透明,使用方便。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class ProcessingStreamExample {
public static void main(String[] args) {
String sourceFile = "source.txt";
String targetFile = "target.txt";
try {
// 使用处理流 BufferedReader 包装节点流 FileReader,提供缓冲功能
BufferedReader reader = new BufferedReader(new FileReader(sourceFile));
// 使用处理流 BufferedWriter 包装节点流 FileWriter,提供缓冲功能
BufferedWriter writer = new BufferedWriter(new FileWriter(targetFile));
String line;
while ((line = reader.readLine()) != null) {
// 将读取的每一行数据写入到新文件
writer.write(line);
writer.newLine(); // 写入换行符
}
reader.close();
writer.close();
System.out.println("File copied successfully!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意事项
- 读写顺序要一致
- 要求实现序列化或反序列化对象,需要实现Serializable
- 序列化的类中建议添加SerialVersionUID,为了提高版本的兼容性
- 序列化对象时,默认将里面所有属性都进行序列化,但除了static或transient修饰的成员
- 序列化对象时,要求里面属性的类型也需要实现序列化接口
- 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化
20.4 输入流
InputStream(字节流)
FileInputStream
FileInputStream是文件字节输入流
FileInputStream
是 Java 中用于从文件中读取数据的输入流类。它允许你从文件中逐个字节或以一定的字节数组大小进行读取操作。
构造函数
FileInputStream
类有多个构造函数,最常用的是以下两个:
FileInputStream(File file)
:通过指定一个File
对象来创建一个文件输入流。FileInputStream(String name)
:通过指定文件的路径名来创建一个文件输入流。
主要方法
FileInputStream
提供了一些常用的方法来从文件中读取字节:int read() throws IOException
:从输入流中读取一个字节的数据。如果已经到达文件末尾,则返回 -1。int read(byte[] b) throws IOException
:从输入流中读取最多b.length
个字节的数据到字节数组b
中。返回实际读取的字节数,如果已经到达文件末尾,则返回 -1。int read(byte[] b, int off, int len) throws IOException
:从输入流中读取最多len
个字节的数据到字节数组b
的偏移量为off
处。返回实际读取的字节数,如果已经到达文件末尾,则返回 -1。void close() throws IOException
:关闭输入流。
使用示例
import java.io.*;
public class FileInputStreamExample {
public static void main(String[] args) {
try {
FileInputStream inputStream = new FileInputStream("example.txt");
int data;
while ((data = inputStream.read()) != -1) {
System.out.print((char) data);
}
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意事项
- 使用完毕后需要关闭文件输入流,以释放资源,避免文件句柄泄漏。
- 文件输入流是以字节为单位读取数据的,如果需要按字符或其他方式读取,可以结合使用
InputStreamReader
等类。 - 在处理大文件时,要注意避免一次性读取全部数据到内存中,而是应该采用适当的缓冲区大小,分块读取数据。
ObjectInputStream:对象字节输入流
BufferedInputStream
BufferedInputStream
缓冲字节输入流,BufferedInputStream上层还有一个FilterInputStream,它提供了缓冲功能,以提高读取效率。
构造函数
BufferedInputStream
的构造函数通常接受一个 InputStream
类型的参数,用于指定要从中读取数据的输入流。下面是构造函数的签名:
public BufferedInputStream(InputStream in)
主要方法
read(byte[] b)
: 从输入流中读取数据到字节数组中。
public int read(byte[] b) throws IOException
read(byte[] b, int off, int len)
: 从输入流中读取数据到指定的字节数组中的指定位置开始。
public int read(byte[] b, int off, int len) throws IOException
skip(long n)
: 跳过输入流中的指定字节数。
public long skip(long n) throws IOException
available()
: 返回输入流中尚未读取的字节数。
public int available() throws IOException
close()
: 关闭BufferedInputStream
,释放资源。
public void close() throws IOException
示例
以下是一个简单的示例,演示了如何使用 BufferedInputStream
从文件中读取数据:
java
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("example.txt"))) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
System.out.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用细节
- 使用
BufferedInputStream
时也要确保及时关闭流,以释放资源。可以使用 try-with-resources 语句来自动关闭流,或者在 finally 块中手动关闭流。 BufferedInputStream
使用缓冲机制将数据从输入流读入内存缓冲区,然后从缓冲区读取数据,这可以减少对底层资源(例如磁盘)的访问次数,提高读取效率。- 使用
available()
方法可以查看输入流中尚未读取的字节数,以便进行相应的处理。 - 与
BufferedReader
和BufferedWriter
类似,BufferedInputStream
也是线程安全的,可以在多线程环境中共享和使用。
ObjectInputStream
当需要从输入流中读取对象数据时,我们可以使用 ObjectInputStream
。它是 Java 中用于反序列化对象的类,可以将对象从输入流中读取并重建。下面是关于 ObjectInputStream
的详细介绍:
构造函数
ObjectInputStream
的构造函数通常接受一个 InputStream
类型的参数,用于指定要从中读取对象数据的输入流。下面是构造函数的签名:
public ObjectInputStream(InputStream in) throws IOException
主要方法
readObject()
: 从输入流中读取一个对象,并将其反序列化为原始对象。
public Object readObject() throws IOException, ClassNotFoundException
close()
: 关闭ObjectInputStream
,释放资源。
public final void close() throws IOException
示例
以下是一个简单的示例,演示了如何使用 ObjectInputStream
从文件中读取对象:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class Main {
public static void main(String[] args) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("example.dat"))) {
Object obj = ois.readObject();
System.out.println("Read object: " + obj);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
使用细节
- 在使用
ObjectInputStream
读取对象之前,对象必须已经被序列化并写入到输出流中,通常是使用ObjectOutputStream
。 - 读取的对象必须是可序列化的,即实现了
Serializable
接口。 - 使用
readObject()
方法读取对象时,必须捕获ClassNotFoundException
异常,因为在反序列化对象时可能会找不到对象的类定义。 - 与其他输入流类似,使用
ObjectInputStream
时也要确保及时关闭流,以释放资源。可以使用 try-with-resources 语句来自动关闭流,或者在 finally 块中手动关闭流。
Reader(字符流)
FileReader
FileReader
是 Java 中用于读取字符文件的类之一,它继承自 InputStreamReader
类,实现了字符流输入。
FileReader
用于以字符为单位读取文件内容,通常用于读取文本文件。
构造函数
FileReader
类有多个构造函数,最常用的是以下两个:
FileReader(File file)
:通过指定一个File
对象来创建一个文件读取流。FileReader(String fileName)
:通过指定文件的路径名来创建一个文件读取流。
主要方法
FileReader
提供了一些常用的方法来读取字符数据:
int read() throws IOException
:从输入流中读取一个字符的数据。如果已经到达文件末尾,则返回 -1。int read(char[] cbuf) throws IOException
:从输入流中读取最多cbuf.length
个字符的数据到字符数组cbuf
中。返回实际读取的字符数,如果已经到达文件末尾,则返回 -1。void close() throws IOException
:关闭输入流。
使用示例
import java.io.FileReader;
import java.io.IOException;
public class FileReaderExample {
public static void main(String[] args) {
try {
FileReader reader = new FileReader("example.txt");
int data;
while ((data = reader.read()) != -1) {
System.out.print((char) data);
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意事项
- 在使用完毕后,需要调用
close()
方法关闭文件读取流,以释放相关资源。 FileReader
是以字符为单位读取数据的,如果需要按字节或其他方式读取,可以结合使用FileInputStream
等类。- 在处理大文件时,要注意避免一次性读取全部数据到内存中,而是应该采用适当的缓冲区大小,分块读取数据。
BufferedReader
BufferedReader
是 Java 中的一个类,用于从输入流中读取数据,并提供了缓冲功能,以提高读取效率。它继承自 Reader
类,因此可以用于读取字符数据。
使用 BufferedReader
可以逐行读取文本文件中的内容,也可以按字符读取。它的构造函数通常接受一个 Reader
类型的参数,用于指定要从中读取数据的输入流,例如 FileReader
。
构造函数
BufferedReader
的构造函数通常接受一个 Reader
类型的参数,用于指定要从中读取数据的输入流。下面是构造函数的签名:
public BufferedReader(Reader in)
主要方法
readLine()
: 用于读取文件中的一行文本,并将其作为字符串返回。当读取到文件末尾时,返回 null。
public String readLine() throws IOException
read()
: 用于读取文件中的下一个字符,并返回其 ASCII 值。当读取到文件末尾时,返回 -1。
public int read() throws IOException
close()
: 用于关闭BufferedReader
,释放资源。
public void close() throws IOException
示例
以下是一个简单的示例,演示了如何使用 BufferedReader
逐行读取文件中的内容:
java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用细节
- 使用
BufferedReader
时,要确保及时关闭流,以释放资源。可以使用 try-with-resources 语句来自动关闭流,或者在 finally 块中手动关闭流。 - 在读取大型文件时,
BufferedReader
可以提高读取效率,因为它利用了缓冲机制,减少了频繁访问磁盘的次数。 readLine()
方法返回的字符串不包括行尾符,例如换行符\n
或回车符\r
。如果需要保留行尾符,可以使用read()
方法逐个字符读取,并手动处理行尾符。BufferedReader
是线程安全的,可以在多线程环境中共享和使用。
InputStreamReader
InputStreamReader
是 Java 中用于将字节流转换为字符流的桥梁类,它继承自 Reader
类。它能够将字节流按照指定的字符编码方式解码为字符流,从而实现从字节流到字符流的转换。下面是对 InputStreamReader
的详细介绍:
构造函数
InputStreamReader
的构造函数通常接受两个参数:一个 InputStream
类型的字节流对象和一个字符串类型的字符编码名称。它将指定的字节流按照指定的字符编码方式解码为字符流。下面是构造函数的签名:
public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException
主要方法
read()
: 从输入流中读取一个字符并返回。如果已经到达流的末尾,则返回 -1。
public int read() throws IOException
read(char[] cbuf, int offset, int length)
: 从输入流中读取数据到指定的字符数组中。
public int read(char[] cbuf, int offset, int length) throws IOException
close()
: 关闭InputStreamReader
,释放资源。
public void close() throws IOException
示例
以下是一个简单的示例,演示了如何使用 InputStreamReader
将字节流转换为字符流,并从输入流中读取数据:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) {
try (InputStreamReader isr = new InputStreamReader(new FileInputStream("example.txt"), "UTF-8")) {
char[] buffer = new char[1024];
int bytesRead;
while ((bytesRead = isr.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, bytesRead));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用细节*
InputStreamReader
通常用于从字节流中读取数据并解码为字符流。可以通过指定字符编码名称来控制解码的方式。- 在构造
InputStreamReader
对象时,如果不指定字符编码名称,则使用平台默认的字符编码。 - 使用
read()
方法从输入流中读取字符数据,可以指定读取到的字符存放的字符数组、偏移量和长度。 - 在使用完
InputStreamReader
后,应该及时调用close()
方法关闭流,释放资源。
20.5 输出流
OutputStream(字节流)
OutputStream
是 Java 中用于写入数据的抽象类之一,它是所有输出流类的超类。它提供了一种向目标输出设备(如文件、网络连接等)写入数据的通用接口。下面是对 OutputStream
的详细介绍:
FileOutputStream
FileOutputStream是文件字节输出流
构造函数
FileOutputStream
类有多个构造函数,最常用的是以下两个:
FileOutputStream(File file)
:通过指定一个File
对象来创建一个文件输出流。FileOutputStream(String name)
:通过指定文件的路径名来创建一个文件输出流。FileOutputStream(File file, boolean append)
:如果append
为true,表示在文件末尾追加数据。FileOutputStream(String name, boolean append)
:如果append
为true,表示再文件末尾追加数据.
主要方法
FileOutputStream
提供了一些常用的方法来向文件写入字节数据:
void write(int b) throws IOException
:向文件中写入一个字节的数据。void write(byte[] b) throws IOException
:向文件中写入字节数组b
中的所有字节。void write(byte[] b, int off, int len) throws IOException
:向文件中写入字节数组b
中从偏移量off
开始、长度为len
的字节。void flush() throws IOException
:刷新输出流,将缓冲区中的数据立即写入文件。void close() throws IOException
:关闭文件输出流。
使用示例
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamExample {
public static void main(String[] args) {
try {
FileOutputStream outputStream = new FileOutputStream("example.txt");
String data = "Hello, world!";
outputStream.write(data.getBytes());
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意事项
- 在使用完毕后,需要调用
close()
方法关闭文件输出流,以释放相关资源。 - 如果希望数据立即被写入文件,可以调用
flush()
方法。 - 如果文件不存在,
FileOutputStream
会创建一个新的文件;如果文件已存在,会覆盖原有内容(除非在构造函数中指定了append
参数为true
)。
BufferedOutputStream
当需要向输出流写入数据时,我们可以使用 BufferedOutputStream
。它提供了缓冲功能,以提高写入效率。
构造函数
BufferedOutputStream
的构造函数通常接受一个 OutputStream
类型的参数,用于指定要写入数据的输出流。下面是构造函数的签名:
public BufferedOutputStream(OutputStream out)
主要方法
write(int b)
: 向输出流中写入一个字节数据。
public void write(int b) throws IOException
write(byte[] b)
: 向输出流中写入字节数组中的数据。
public void write(byte[] b) throws IOException
write(byte[] b, int off, int len)
: 向输出流中写入指定字节数组的一部分数据。
public void write(byte[] b, int off, int len) throws IOException
flush()
: 将缓冲区中的数据写入到目标设备(通常是磁盘)。
public void flush() throws IOException
close()
: 关闭BufferedOutputStream
,释放资源。
public void close() throws IOException
示例
以下是一个简单的示例,演示了如何使用 BufferedOutputStream
将数据写入文件:
java
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("example.txt"))) {
String data = "Hello, world!";
byte[] bytes = data.getBytes();
bos.write(bytes);
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用细节
- 与
BufferedInputStream
类似,使用BufferedOutputStream
时也要确保及时关闭流,以释放资源。可以使用 try-with-resources 语句来自动关闭流,或者在 finally 块中手动关闭流。 BufferedOutputStream
使用缓冲机制将数据写入内存缓冲区,然后在适当的时候将数据写入目标设备,例如磁盘。这可以提高写入效率,特别是对于频繁写入小量数据的情况。- 使用
flush()
方法可以将缓冲区中的数据立即写入到目标设备,而不必等到缓冲区满或者关闭流时才写入。 - 与
BufferedInputStream
、BufferedReader
和BufferedWriter
类似,BufferedOutputStream
也是线程安全的,可以在多线程环境中共享和使用。
ObjectOutputStream
ObjectOutputStream
是 Java 中用于序列化对象的类,它可以将对象转换为字节流,以便于存储或传输。下面是关于 ObjectOutputStream
的详细介绍:
构造函数
ObjectOutputStream
的构造函数通常接受一个 OutputStream
类型的参数,用于指定要将对象数据写入的输出流。下面是构造函数的签名:
public ObjectOutputStream(OutputStream out) throws IOException
主要方法
writeObject(Object obj)
: 将对象写入输出流,并序列化为字节流。
public final void writeObject(Object obj) throws IOException
flush()
: 将缓冲区中的数据写入到目标设备(通常是磁盘)。
public void flush() throws IOException
close()
: 关闭ObjectOutputStream
,释放资源。
public void close() throws IOException
示例
以下是一个简单的示例,演示了如何使用 ObjectOutputStream
将对象写入文件:
java
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Main {
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("example.dat"))) {
MyClass obj = new MyClass("John", 25);
oos.writeObject(obj);
System.out.println("Object written successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用细节
- 要写入的对象必须是可序列化的,即实现了
Serializable
接口。 - 使用
ObjectOutputStream
将对象写入输出流时,会将对象转换为字节流,并将其写入到输出流中。因此,输出流必须支持写入字节数据,例如文件输出流。 - 在写入对象之前,通常会先调用
flush()
方法将缓冲区中的数据写入到目标设备,以确保数据被及时写入到输出流中。 - 与其他输出流类似,使用
ObjectOutputStream
时也要确保及时关闭流,以释放资源。可以使用 try-with-resources 语句来自动关闭流,或者在 finally 块中手动关闭流。
PrintStream
PrintStream
是 Java 中用于向输出流中打印数据的类,它继承自 OutputStream
类。PrintStream
提供了一系列用于打印各种数据类型的方法,并且自带缓冲区,可以提高输出效率。下面是对 PrintStream
的详细介绍:
构造函数
PrintStream
的构造函数通常接受一个 OutputStream
类型的参数,用于指定要将数据写入的输出流。下面是构造函数的签名:
public PrintStream(OutputStream out)
主要方法
print()
: 向输出流中打印数据,不会添加换行符。
public void print(boolean b)
public void print(char c)
public void print(int i)
public void print(long l)
public void print(float f)
public void print(double d)
public void print(char[] s)
public void print(String s)
println()
: 向输出流中打印数据,并在结尾处添加换行符。
public void println(boolean b)
public void println(char c)
public void println(int i)
public void println(long l)
public void println(float f)
public void println(double d)
public void println(char[] s)
public void println(String s)
public void println()
printf()
: 格式化输出,类似于 C 语言中的printf()
函数。
public PrintStream printf(String format, Object... args)
close()
: 关闭PrintStream
,释放资源。
public void close()
示例
以下是一个简单的示例,演示了如何使用 PrintStream
向输出流中打印数据:
java
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
public class Main {
public static void main(String[] args) {
try (PrintStream ps = new PrintStream(new FileOutputStream("example.txt"))) {
ps.println("Hello, world!");
ps.printf("The value of %s is %d", "num", 10);
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用细节
PrintStream
是 Java 中用于打印数据到输出流的类,它可以方便地向输出流中打印各种数据类型的数据。- 使用
print()
方法打印数据时,不会在结尾处添加换行符;而使用println()
方法打印数据时,会在结尾处添加换行符。 - 使用
printf()
方法可以进行格式化输出,类似于 C 语言中的printf()
函数,可以使用占位符指定输出的格式。 - 在使用完
PrintStream
后,应该及时调用close()
方法关闭流,释放资源。
Writer(字符流)
FileWriter
FileWriter
是 Java 中用于向文件写入字符数据的类,它继承自 OutputStreamWriter
类,实现了字符流输出。FileWriter
主要用于以字符为单位向文件写入数据,通常用于写入文本文件。
构造函数
FileWriter
类有多个构造函数,最常用的是以下两个:
FileWriter(File file)
:通过指定一个File
对象来创建一个文件写入流。FileWriter(String fileName)
:通过指定文件的路径名来创建一个文件写入流。
主要方法
FileWriter
提供了一些常用的方法来写入字符数据:
void write(int c) throws IOException
:将指定的字符写入文件。void write(String str) throws IOException
:将指定的字符串写入文件。void write(char[] cbuf) throws IOException
:将字符数组中的所有字符写入文件。void flush() throws IOException
:刷新输出流,将缓冲区中的数据立即写入文件。void close() throws IOException
:关闭文件写入流。
使用示例
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterExample {
public static void main(String[] args) {
try {
FileWriter writer = new FileWriter("example.txt");
String data = "Hello, world!";
writer.write(data);
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意事项
- 在使用完毕后,需要调用
close()
方法关闭文件写入流,以释放相关资源。 - 如果希望数据立即被写入文件,可以调用
flush()
方法。 - 如果文件不存在,
FileWriter
会创建一个新的文件;如果文件已存在,会覆盖原有内容(除非在构造函数中指定了append
参数为true
)。
BufferedWriter
BufferedWriter
是 Java 中用于写入文本数据的类,提供了缓冲功能以提高写入效率。
构造函数
BufferedWriter
的构造函数通常接受一个 Writer
类型的参数,用于指定要写入数据的输出流。下面是构造函数的签名:
public BufferedWriter(Writer out)
主要方法
write(String str)
: 用于将字符串写入文件。
public void write(String str) throws IOException
newLine()
: 写入一个行分隔符。不同操作系统使用不同的行分隔符,newLine()
方法会根据当前操作系统自动写入相应的行分隔符。
public void newLine() throws IOException
flush()
: 将缓冲区中的数据写入到目标设备(通常是磁盘)。
public void flush() throws IOException
close()
: 关闭BufferedWriter
,释放资源。
public void close() throws IOException
示例
以下是一个简单的示例,演示了如何使用 BufferedWriter
将数据写入文件:
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("example.txt"))) {
bw.write("Hello, world!");
bw.newLine();
bw.write("This is a new line.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用细节
- 与
BufferedReader
类似,使用BufferedWriter
时也要确保及时关闭流,以释放资源。可以使用 try-with-resources 语句来自动关闭流,或者在 finally 块中手动关闭流。 BufferedWriter
使用缓冲机制将数据写入内存缓冲区,然后在适当的时候将数据写入目标设备,例如磁盘。这可以提高写入效率,特别是对于频繁写入小量数据的情况。- 使用
newLine()
方法可以确保在不同操作系统下写入的行分隔符是正确的,从而使得文件在不同系统中都具有良好的可读性。 - 与
BufferedReader
类似,BufferedWriter
也是线程安全的,可以在多线程环境中共享和使用。
OutputStreamWriter
OutputStreamWriter
是 Java 中用于将字符流转换为字节流的桥梁类,它继承自 Writer
类。它能够将字符流按照指定的字符编码方式编码为字节流,从而实现从字符流到字节流的转换。下面是对 OutputStreamWriter
的详细介绍:
构造函数
OutputStreamWriter
的构造函数通常接受两个参数:一个 OutputStream
类型的字节流对象和一个字符串类型的字符编码名称。它将指定的字符流按照指定的字符编码方式编码为字节流。下面是构造函数的签名:
public OutputStreamWriter(OutputStream out, String charsetName) throws UnsupportedEncodingException
主要方法
write(int c)
: 将一个字符写入输出流。
public void write(int c) throws IOException
write(char[] cbuf, int off, int len)
: 将字符数组的一部分写入输出流。
public void write(char[] cbuf, int off, int len) throws IOException
write(String str, int off, int len)
: 将字符串的一部分写入输出流。
public void write(String str, int off, int len) throws IOException
flush()
: 将缓冲区中的数据写入到目标设备(通常是磁盘)。
public void flush() throws IOException
close()
: 关闭OutputStreamWriter
,释放资源。
public void close() throws IOException
示例
以下是一个简单的示例,演示了如何使用 OutputStreamWriter
将字符流转换为字节流,并将数据写入到输出流中:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
public class Main {
public static void main(String[] args) {
try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("example.txt"), "UTF-8")) {
String data = "Hello, world!";
osw.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用细节
OutputStreamWriter
通常用于将字符流转换为字节流,并将字符数据按照指定的字符编码方式编码为字节数据。- 在构造
OutputStreamWriter
对象时,如果不指定字符编码名称,则使用平台默认的字符编码。 - 使用
write()
方法将字符数据写入输出流中,该方法会根据指定的字符编码方式将字符转换为字节并写入输出流。 - 在使用完
OutputStreamWriter
后,应该及时调用close()
方法关闭流,释放资源。
PrintWriter
PrintWriter
是 Java 中用于向输出流中写入字符数据的类,它继承自 Writer
类。PrintWriter
提供了一系列用于打印各种数据类型的方法,并且自带缓冲区,可以提高输出效率。下面是对 PrintWriter
的详细介绍:
构造函数
PrintWriter
的构造函数通常接受一个 OutputStream
类型的参数,用于指定要将数据写入的输出流。下面是构造函数的签名:
public PrintWriter(OutputStream out)
主要方法
print()
: 向输出流中打印数据,不会添加换行符。
public void print(boolean b)
public void print(char c)
public void print(int i)
public void print(long l)
public void print(float f)
public void print(double d)
public void print(char[] s)
public void print(String s)
println()
: 向输出流中打印数据,并在结尾处添加换行符。
java
public void println(boolean b)
public void println(char c)
public void println(int i)
public void println(long l)
public void println(float f)
public void println(double d)
public void println(char[] s)
public void println(String s)
public void println()
printf()
: 格式化输出,类似于 C 语言中的printf()
函数。
public PrintWriter printf(String format, Object... args)
close()
: 关闭PrintWriter
,释放资源。
public void close()
示例
以下是一个简单的示例,演示了如何使用 PrintWriter
向输出流中写入字符数据:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
public class Main {
public static void main(String[] args) {
try (PrintWriter pw = new PrintWriter(new FileOutputStream("example.txt"))) {
pw.println("Hello, world!");
pw.printf("The value of %s is %d", "num", 10);
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用细节
PrintWriter
是 Java 中用于向输出流中写入字符数据的类,它可以方便地向输出流中打印各种数据类型的数据。- 使用
print()
方法打印数据时,不会在结尾处添加换行符;而使用println()
方法打印数据时,会在结尾处添加换行符。 - 使用
printf()
方法可以进行格式化输出,类似于 C 语言中的printf()
函数,可以使用占位符指定输出的格式。 - 在使用完
PrintWriter
后,应该及时调用close()
方法关闭流,释放资源。
20.6 Properties类
Properties
类是 Java 中用于处理属性集的类,它继承自 Hashtable<Object, Object>
类。Properties
类主要用于管理键值对形式的配置信息,通常用于读取和写入配置文件。下面是对 Properties
类的详细介绍:
主要方法
setProperty(String key, String value)
: 设置指定键的值。
public synchronized Object setProperty(String key, String value)
getProperty(String key)
: 获取指定键对应的值。
public String getProperty(String key)
getProperty(String key, String defaultValue)
: 获取指定键对应的值,如果键不存在,则返回默认值。
public String getProperty(String key, String defaultValue)
load(InputStream inStream)
: 从输入流中加载属性列表。
public synchronized void load(InputStream inStream) throws IOException
store(OutputStream out, String comments)
: 将属性列表写入输出流。
public synchronized void store(OutputStream out, String comments) throws IOException
list(PrintStream out)
: 将属性列表输出到指定的输出流。
public void list(PrintStream out)
list(PrintWriter out)
: 将属性列表输出到指定的输出流。
public void list(PrintWriter out)
示例
以下是一个简单的示例,演示了如何使用 Properties
类读取和写入配置文件:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
public class Main {
public static void main(String[] args) {
// 读取配置文件
Properties props = new Properties();
try (FileInputStream fis = new FileInputStream("config.properties")) {
props.load(fis);
String username = props.getProperty("username");
String password = props.getProperty("password");
System.out.println("Username: " + username);
System.out.println("Password: " + password);
} catch (IOException e) {
e.printStackTrace();
}
// 写入配置文件
try (FileOutputStream fos = new FileOutputStream("config.properties")) {
props.setProperty("username", "admin");
props.setProperty("password", "123456");
props.store(fos, "Updated configuration");
System.out.println("Configuration updated successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用细节
Properties
类可以用于读取和写入配置文件,配置文件通常以.properties
为后缀名,使用键值对的形式存储配置信息。- 使用
setProperty()
方法可以设置指定键的值,使用getProperty()
方法可以获取指定键的值。 - 使用
load()
方法可以从输入流中加载属性列表,通常用于读取配置文件;使用store()
方法可以将属性列表写入输出流,通常用于更新配置文件。 Properties
类还提供了list()
方法,可以将属性列表输出到指定的输出流,方便查看属性内容。- 在读取和写入配置文件时,通常需要使用 try-with-resources 语句来确保输入流和输出流及时关闭,以释放资源。
20.7 标准输入输出流
Java 的标准输入输出流是用于与外部环境进行数据交互的重要部分。标准输入流 System.in
和标准输出流 System.out
是 Java 标准库提供的两个常用的流对象。
标准输入流
标准输入流 System.in
是一个 InputStream
对象,用于从控制台读取输入的数据。它通常用于从用户输入中读取数据,例如从键盘输入。
主要方法
read()
: 从标准输入流中读取单个字节的数据。返回值为读取到的字节的 ASCII 值,或者在达到流的末尾时返回 -1。read(byte[] b)
: 从标准输入流中读取数据到指定的字节数组中。close()
: 关闭标准输入流,释放资源。
示例
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try {
System.out.println("Enter some text: ");
int data = System.in.read();
System.out.println("You entered: " + (char)data);
} catch (IOException e) {
e.printStackTrace();
}
}
}
标准输出流
标准输出流 System.out
是一个 PrintStream
对象,用于向控制台输出数据。它通常用于向用户显示信息或调试程序。
主要方法
print()
: 向标准输出流中打印数据,不换行。println()
: 向标准输出流中打印数据,并在结尾处添加换行符。printf()
: 格式化输出,类似于 C 语言中的printf()
函数。close()
: 关闭标准输出流,释放资源。
示例
public class Main {
public static void main(String[] args) {
System.out.println("Hello, world!");
System.out.print("This is ");
System.out.println("a test.");
int num = 10;
System.out.printf("The value of num is: %d\n", num);
}
}
使用细节
- 标准输入输出流通常用于与用户交互,例如从控制台读取输入并向控制台输出结果。
- 在使用标准输入流时,通常需要使用输入流的
read()
方法读取数据,然后进行适当的类型转换。 - 在使用标准输出流时,通常使用输出流的
print()
、println()
或printf()
方法打印数据到控制台。 - 标准输入输出流是 Java 应用程序与用户交互的重要方式,在开发命令行工具和简单的交互式应用程序时特别有用。