文章目录
- 零、前言
- 一、Java环境
- 二、常量与变量
- 三、运算符
- 四、流程控制
- 五、数组
- 六、方法
- 七、面向对象
- 八、封装
- 九、方法参数与命令行参数
- 十、继承
- 十一、抽象、接口
- 十二、多态
- 十三、代码块
- 十四、内部类的使用
- 十五、异常
- 十六、Object类
- 十七、String类
- 十八、包装类
- 十九、多线程
- 二十、集合
零、前言
字节
字节是计算机中存储数据的最小单位,用byte或B表示,当然,计算机中最小的存储单元是二进制。二进制使用比特(bit)作为单位,有:
1
B
=
8
b
i
t
1B=8bit
1B=8bit
1024
B
=
1
K
B
1024B=1KB
1024B=1KB
1024
K
B
=
1
M
B
1024KB=1MB
1024KB=1MB
1024
M
B
=
1
G
B
1024MB=1GB
1024MB=1GB
常用dos命令
一、Java环境
1.1JVM和跨平台介绍
JVM(Java虚拟机)是Java运行程序的假想计算机,主要用来运行Java程序。跨平台是指Java代码可以在不同的操作系统上运行(一次编写,到处运行),如Windows、Linux、Mac等操作系统,只需安装不同版本的JVM,即可将Java程序翻译为系统可运行的程序,而不需要编写不同的Java程序。
1.2 JDK和JRE
- JDK(Java Development Kit):Java开发工具包,如:
- javac:编译工具。
- java:运行工具。
- jdb:调试工具。
- jhat:内存分析工具。
- JRE(Java Runtime Environment):Java运行环境,包含JVM以及后面开发用到的核心类库。
事实上,JDK当中包含了JRE,故安装Java时只需安装JDK即可。JDK、JRE与JVM的关系如下:
注意,从jdk9开始,jdk目录中就没有单独的jre目录了,因为jre作为一个运行环境,里面不需要太多的东西而浪费空间,降低运行效率。故而在jdk9中引用了模块化技术,让开发者能按照自己的应用创建一个最小的运行环境(如一个微服务的部署只需要一个非常小的runtime,而不是像以前一样不管应用复杂还是简单,都需要一个近百兆的jre环境)。
1.3jdk的安装与环境变量的配置
1.4Java入门程序
开发的三步骤:
- 编写:创建一个文本文档,将后缀名改为.java,使其变为一个java文件。
public class helloworld{
public static void main(String[] args){
System.out.println("Hello World");
}
}
- 编译:使用javac命令进行编译,生成一个.class文件(字节码文件)。
Microsoft Windows [版本 10.0.22631.3447](c) Microsoft Corporation。保留所有权利。
C:\Users\lenovo>cd desktop
C:\Users\lenovo\Desktop>javac helloworld.java
- 运行:使用java命令在jvm当中运行字节码文件(不需要带后缀名)。
C:\Users\lenovo\Desktop>java helloworld
Hello World
C:\Users\lenovo\Desktop>
1.5注释
注释是用于对代码的解释说明,有单行注释、多行注释与文档注释。
单行注释:
//注释内容
多行注释:
/*
多行注释
*/
文本注释:
/**
注释内容
*/
其中,文档注释的作用在于,将来给别人一个开发好的类时,可快速进行了解类的功能等信息。文档注释的内容可根据javadoc命令快速生成生成(API文档)。
javadoc -d 要生成的文件夹名字 -author -version 文件夹.java
例:
helloworld01.java
/**
此类是一个java入门程序
@author 悟兰因
@version v1.0
*/
public class helloworld01{
/**
main是程序的入口,jvm执行代码时会从main开始执行
*/
public static void main(String[] args){
System.out.println("Hello World");
}
}
使用命令生成说明文档:
javadoc -d api -author -version helloworld01.java
打开index.html:
/**
1.public class helloworld:定义一个类
2.class:代表的就是类,类是java程序最基本的组成单元,所有代码都需要在类中写
3.class后面跟的名字叫做类名,类名要和java文件名保持一致
*/
public class helloworld01{
/**
main是程序的入口,jvm执行代码时会从main开始执行
*/
public static void main(String[] args){
//打印输出语句,将内容打印到控制台上
System.out.println("Hello World");
}
}
其中,关键字是java提前定义的,具有特定含义的小写单词,
1.6字符编码问题
- 编码:保存数据的过程就是编码的过程。
- 解码:读取数据的过程就是解码的过程。
注意,编码和解码所遵守的编码规范必须是一样的,常见的编码规范有:
- GBK:专门为中文所设计的编码,一个中文汉字在GBK中占两个字节。
- UTF-8:一个中文汉字占3个字节。
dos命令窗口默认编码为GBK。
1.7java文件名和类名问题
类名并非一定要与java文件名一致,但若类名与java文件名不一致,需要将class前面的public去掉,并且,main方法必须写在public的类中。
helloworld02.java
public class helloworld{
public static void main(String[] args){
System.out.println("你好");
}
}
编译程序:
C:\Users\lenovo\Desktop>javac helloworld02.java
helloworld02.java:1: 错误: 类 helloworld 是公共的, 应在名为 helloworld.java 的文件中声明
public class helloworld{
^
1 个错误
即,若class前面带有public,此时类名必须和java文件名一致。事实上,一个java文件可以有多个class类,但只能有一个类带有public(程序的入口必须唯一)
public class helloworld02{
public static void main(String[] args){
System.out.println("你好");
}
}
class A{}
class B{}
此时会为每个class类都生成一个字节码文件。
1.8常用dos命令
功能 | dos代码 |
---|---|
切换盘符 | 盘符: ->回车,盘符不区分大小写 |
查看当前路径下的所有文件 | dir |
进入到指定文件夹下 | cd 文件夹名字 |
进入到多级文件夹下 | cd 文件夹名字/文件夹名字 |
退到上一级目录 | cd .. |
直接退到磁盘根目录 | cd \ |
清屏 | cls |
退出cmd | exit |
创建文件夹 | mkdir 文件夹名 |
创建多级文件夹 | mkdir 文件夹名\文件夹名 |
删除文件夹 | rd 文件夹名(文件夹必须为空) |
二、常量与变量
2.1常量
在代码运行过程中,值不会发生改变的数据称为常量,常量可分为整数常量、小数常量、字符常量(带单引号,且单引号内有且仅有一个内容,单引号内不能为空)、字符串常量(带双引号,双引号中内容随意,可无任何内容)、布尔常量(true与false)及空常量(null,代表数据不存在)。
/**
* @author 悟兰因
* @date 2024/1/1
* @package PACKAGE_NAME
*/
import java.util.Scanner;
public class demo {
public static void main(String[] args){
//整数常量
System.out.println(1);
System.out.println(-1);
//小数常量
System.out.println(1.4);
System.out.println(2.3);
//字符常量
System.out.println(' ');//空格字符
System.out.println('1');
//字符串常量
System.out.println("");
System.out.println("Hello World!");
//布尔常量
System.out.println(true);
System.out.println(false);
//空常量null不能直接使用
}
}
常见常量之间的运算:
public class demo {
public static void main(String[] args){
System.out.println(10+3);//13
System.out.println(10-3);//7
/*
* 前后若都是整数,则结果只取整数部分、
* 前后只要有一个数带浮点数,则结果就是正常小数
* */
System.out.println(10/3);//3
System.out.println(10/0.3);
System.out.println(10/3.0);
}
}
2.2变量与基本数据类型
在代码运行过程中,值会随着不同的情况而随时发生改变的数据,称为变量。变量的数据类型可分为基本数据类型与引用数据类型,其中基本数据类型包含四类八种:
- 整型:byte、short、int、long
- 浮点型:float、double
- 字符型:char
- 布尔型:boolean
(可发现并没有字符串,事实上,字符串属于引用数据类型,用String表示。String本质上是一个类,只不过字符串在定义时,其格式与基本数据类型相同)
基本数据类型的默认值:
数据类型 | 默认值 |
---|---|
整数 | 0 |
小数 | 0.0 |
字符类型 | ‘\u000’(ASCII值为0对应的字符) |
布尔 | false |
引用数据类型包含类、数组、接口、枚举、注解五种,且引用数据类型的默认值为null。
定义方式:
1.数据类型 变量名=值;
int i=1;
2.数据类型 变量名;变量名=值;
int i;
i=1;
3.数据类型 变量名1,变量名2,变量名3;变量名1=值;变量名2=值;变量名3=值;
int i,j,k;
i=1;
j=2;
k=3;
//或者
int l=1,m=2,n=3;
注意:当等号右边有运算时,应当将结果赋值给等号左边的变量。
需注意几种基本数据类型的特殊定义方式:
public static void main(String[] args){
//long:定义long类型时需在变量后加上L
long num1=10L;
System.out.println(num1);
//float:定义float类型时需在变量后加上F
float num2=2.5F;
//double:定义时无需加上后缀,是小数的默认类型
double num3=2.3;
}
float与double的区别:
- float的小数位只有23位二进制,能表示的最大十进制为 2 23 2^{23} 223,所以float代表的小数小数位为7位(即有效位数为7位)。而double的小数位为52位二进制,故而double的精度能高。
public static void main(String[] args){
float a=10F;
float b=3F;
float res1=a/b;
System.out.println(res1);//3.3333333(7位)
double c=10;
double d=3;
double res2=c/d;
System.out.println(res2);//3.3333333333333335(16位)
float x=3.55F;
float y=2.12F;
float res3=x-y;
System.out.println(res3);//1.4300001
}
注意:在开发当中不应用float或double直接参与运算,因为可能会出现精度损失问题。
变量使用时的注意事项
- 变量不初始化不能使用。
- 在同一个作用域(一个{}下)中不能定义重名的变量。
- 不同作用域中的数据尽量不要随意相互访问。
- 在小作用域中能直接访问大作用域中的变量,反之不可行。
2.3转义字符的使用
转义字符可简单理解为:
- 将普通字符转成具有特殊含义的字符。
- 将具有特殊含义的字符转成普通字符。
public static void main(String[] args){
/*
* n:普通字符
* \n:转义字符
* */
System.out.println("夜来风雨声\n花落知多少\n");
/*
* t:普通字符
* \t:制表符(tab键)
* */
System.out.println("Hello\tWorld");
/*
* 用String表示一个路径,直接将"C:\Users\lenovo\Desktop\api"赋值给path时会因转义字符而报错
* ,故使用转义字符将其转义字符转义为普通字符.即,java中两个\表示一个\字符
* */
String path="C:\\Users\\lenovo\\Desktop\\api";
}
2.4标识符
给类、方法、变量取的名字就是标识符。
- 硬性规定(必须遵守):
- 标识符可包含英文字母、数字、$和_。
- 标识符不能以数字开头。
- 标识符不能是关键字(Java语言中提前规定好的标识符,如static、public)。
- 软性规定(建议遵守):
- 给类命名时,每个单词首字母大写,如DemoHelloWorld。
- 给方法与变量命名时,遵循小驼峰格式,即从第二个单词开始首字母大写。
2.5数据类型转换
当不同类型数据在进行运算、等号量变类型不相等时,会发生一定的数据类型转换,包含隐式数据类型转换与强制数据类型转换两种。
事实上,在定义long、float等变量时,常量后加上L、F是因为常量在默认情况下是int、double类型,若不加上L、F(即使用long、float类型的常量),就会发生类型转换,可能会造成精度丢失。
public static void main(String[] args){
float num=2.5;
}
基本数据类型中按取值范围从小到大进行排序:
b
y
t
e
、
s
h
o
r
t
、
c
h
a
r
−
>
i
n
t
−
>
l
o
n
g
−
>
f
l
o
a
t
−
>
d
o
u
b
l
e
byte、short、char->int->long->float->double
byte、short、char−>int−>long−>float−>double
2.5.1隐式数据类型转换
原则(小转大):
- 将取值范围小的数据类型赋值给范围大的数据类型时,会先将小的数据类型转化为大的数据类型再赋值。
- 取值范围小的数据类型和取值范围大的数据类型运算时,会先将小的数据转化为大的数据类型再运算。
public class Main {
public static void main(String[] args) {
//等号左边是long,右边是int,此时会发生隐式类型转换
long num1=100;
System.out.println(num1);//输出10
//int取值范围小于double,i发生隐式类型转换为double与b进行运算
int i=10;
double b=2.5;
System.out.println(i+b);//输出12.5
}
}
2.5.2强制数据类型转换
当取值范围大的数据类型赋值给取值范围小的数据类型时,需要进行强制数据类型转换。转换格式:
取值范围小的数据类型 变量名=(取值范围小的数据类型)取值范围大的数据类型
package org.example;
public class Main {
public static void main(String[] args) {
//将double类型的2.5赋值给float类型的num1变量
float num1=2.5;
//应该为强制转换:float num1=(float)2.5;或float num1=2.5F;
System.out.println(num1);
}
}//抛出异常: java: 不兼容的类型: 从double转换到float可能会有损失
byte与short的不同之处
byte与short较为特殊 :
- 在定义时若等号右边是整数常量且不超出byte和short的取值范围,就无需强制类型转换,此时虚拟机会自动完成类型转换。
- 若等号右边有变量参与,则byte与short会自动提升为int,然后结果再次赋值给byte或short的变量,需要手动强转。
package org.example;
public class Main {
public static void main(String[] args) {
byte b=10;
System.out.println(b);//输出10,不会报错
b=b+1;//11并未超出byte范围,但b与1(int类型)运算时会提升为int类型
//正确写法为:b=(byte)(b+1);
System.out.println(10);//报错: java: 不兼容的类型: 从int转换到byte可能会有损失
}
}
char的不同之处
char类型数据如果参与运算,会自动提升为int型,若char类型的字符提升为int类型,即为该字符对应的ASCII码。若ASCII表中无对应的int值(如汉字),会去unicode当中查找。
package org.example;
public class Main {
public static void main(String[] args) {
char c='a';
System.out.println(c+1);//输出97
}
}
三、运算符
3.1算数运算符
注意,+
不仅仅用于运算,还可用于字符串拼接。即,任何类型的数据遇到字符串都会变成字符串,此时+
不再是运算符,而其拼接作用。如:
package org.example;
public class Main {
public static void main(String[] args) {
int i=10;
int j=3;
System.out.println(i+j+"");//输出13
System.out.println(i+""+j);//输出103
System.out.println(""+i+j);//输出103(运算从左向右进行)
System.out.println(i+j+""+1);//输出131
System.out.println("i与j相加结果为:"+(i+j));//输出13
//错误写法:System.out.println("i与j相加结果为:"+i+j);因为运算从左向右进行,故会输出103
}
}
特殊的算数运算符:自增自减
格式 | 作用 |
---|---|
变量++ | 后自增 |
++变量 | 先自增 |
变量– | 后自减 |
–变量 | 先自减 |
package org.example;
public class Main {
public static void main(String[] args) {
int i=100;
int result1=++i;//先运算再赋值
System.out.println("result="+result1);//输出101,且i=result=101
result1=i++;//先赋值再运算
System.out.println("result="+result1);//输出101,且i=102,result=101
System.out.println(++result1);//输出102,且result=102
System.out.println(result1++);//输出102,且result=103
int x=10;
int y=20;
int result2=x++ + --y + ++x;
/*
* 注意.自增自减运算符属于单元运算符.与负号相同.优先级大于二元运算符.
* 从左向右运算,(x++)得到x的原值参与result2运算,但在与y运算时x已自增且等于11,即result2=10+19+12
* 且运算后:x=12,y=19
* */
System.out.println("result2="+result2);//输出41
}
}
企业真题:
package org.example;
public class Main {
public static void main(String[] args) {
int c=10;
c=c++;
System.out.println(c);//输出10而非11
}
}
使用反编译得到:
int c=10;
byte var1=c;//var1=10
int var2=c+1;//var2=11
c=var1;//c=10
System.out.println(c);//输出10
3.2赋值运算符
注意:byte、short遇到复合赋值运算符时,jvm会自动转型。
package org.example;
public class Main {
public static void main(String[] args) {
byte b=10;
b+=1;
//相等于b=(byte)(b+1);,但写成b=b+1;是错的,因为需要强转
System.out.println(b);//输出11
}
}
3.3比较运算符
关系运算符的运算结果为boolean类型,可用于条件判断。
3.4逻辑运算符
逻辑运算符用于连接多个boolean类型运算结果,且逻辑运算结果也为boolean类型。
运算符的短路性
3.5三元运算符
格式:
boolean表达式?表达式1:表达式2
先判断,若boolean表达式结果为true,则执行表达式1,否则执行表达式2。例:
int score=59;
String result=score>=60?"及格":"不及格";//result="不及格0"
四、流程控制
4.1Scanner键盘录入
4.1.1Scanner的基本使用
Scanner时Java自定义的一个类,用于将数据通过键盘录入的形式输入到代码中参与运行。由于Scanner在java.util包下,故在使用之前需要导入该包来使用Scanner类:
import java.util.Scanner;
导入该类后需要创建该类的对象,并调用方法,实现键盘录入。Scanner类常用方法:
- Scanner对象.next():录入一个String型变量。
- Scanner对象.nextInt():录入一个int型变量。
package org.example;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
//创建Scanner对象
Scanner sc = new Scanner(System.in);
//调用方法,实现键盘录入
int data1=sc.nextInt();//录入int型整数
System.out.println("data1: "+data1);
String data2=sc.next();//录入String型字符串
System.out.println("data2: "+data2);
}
}
4.1.2next()与nextLine()的区别
package org.example;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String data1=sc.next();
System.out.println("data1: "+data1);
}
}
可见,data1只能接收到空格前的输入。二者区别如下:
- next():录入字符串,遇到空格和回车就结束录入。
- nextLine():录入字符串,遇到回车就结束录入。
package org.example;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String data1=sc.next();
System.out.println("data1: "+data1);
String data2=sc.nextLine();
System.out.println("data2: "+data2);
}
}
4.2Random随机数
Random类是Java自带的一个类,可以在指定的范围内随机生成一个整数,该类处于java.util包下。常用方法为:
- nextInt():在int的取值范围内随机生成一个整数。
可输入参数,在一定整数范围内生成一个随机整数。
package org.example;
import java.util.Random;
public class Main {
public static void main(String[] args) {
Random rand = new Random();
int data=rand.nextInt(10);//在0~9范围内随机生成一个整数
System.out.println("Random number: "+data);
}
}
- 在1~10之间生成一个数:nextInt(10)+1。
- 在1~100之间生成一个数:next(100)+1。
- 在100~999之间生成一个数:next(900)+100。
4.3switch语句
使用格式:
switch(变量){
case 常量值1:
执行语句1;
break;
case 常量2:
执行语句2;
break;
...
default:
执行语句n;
break;
}
用变量接收的值和下面case后面的常量进行匹配,并执行匹配上的执行语句。若所有case后的常量值均匹配不上,则执行default后的语句,其中,break表示结束switch条件判断的执行。
package org.example;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
switch (n){
case 1:
System.out.println("Java");
break;
case 2:
System.out.println("Python");
break;
case 3:
System.out.println("C");
break;
default:
System.out.println("Chinese");
break;
}
}
}
注意,switch可以匹配byte、short、int、char、枚举类型、String类型,但不能匹配float、double类型,因为switch是精准匹配,浮点型本身有精度损失问题,不能用于switch匹配(会直接报错)。
case的穿透性:若没有break,则会出现case的穿透性,程序会一直向下穿透,直到遇到break或switch代码全部执行完。可利用此特性简化代码:
package org.example;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int month = sc.nextInt();
switch (month){
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
System.out.println("上半年");
break;
case 7:
case 8:
case 9:
case 10:
case 11:
case 12:
System.out.println("下半年");
break;
default:
System.out.println("输入错误");
break;
}
}
}
4.4if语句
第一种格式:
if(boolean表达式){
执行语句;
}
//先执行if后的boolean表达式,若为true,则执行执行语句。
第二种格式:
if(boolean表达式1){
执行语句1;
}else{
执行语句;2
}
先执行if后的boolean表达式,若为true,则执行执行语句1,否则执行执行语句2.
第三种格式:
if(boolean表达式1){
执行语句1;
}else if(boolean表达式2){
执行语句2;
}else if(boolean表达式3){
执行语句3;
}else{
执行语句4;
}
//从if开始往下逐个判断,若有boolean表达式结果为true,则执行对应语句并退出判断,若均为false。则执行执行语句4.
例:
package org.example;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int data1 = sc.nextInt();
int data2 = sc.nextInt();
if(data1 > data2) {
System.out.println("data1更大");
}else if(data1 < data2) {
System.out.println("data2更大");
}else{
System.out.println("data1与data2一样大");
}
}
}
4.5for循环
可使用for循环实现代码的循环执行,for循环格式为:
for(初始化变量;比较;步进表达式){
循环语句;
}
执行流程:
- 先走初始化变量。
- 比较若是true,则走循环语句、步进表达式(使初始化变量的值发生变化)后重新执行当前操作,若为false,则循环结束。
package org.example;
public class Main {
public static void main(String[] args) {
//统计1~100的偶数个数
int count=0;
for(int i=1;i<=100;i++){
if(i%2==0)count++;
else continue;
}
System.out.println("1~100共有"+count+"个偶数");
}
}
4.6while循环
格式:
初始化变量
while(比较){
循环语句;
步进表达式;
}
执行流程:
- 初始化变量。
- 比较,若为true,则走循环语句、步进表达式并再次执行当前操作;若为false,则跳出循环。
package org.example;
public class Main {
public static void main(String[] args) {
//统计1~100的偶数个数
int count=0;
int i=1;
while(i<=100){
if(i%2==0)count++;
i++;
}
System.out.println("1~100共有"+count+"个偶数");
}
}
4.7do…while
格式:
初始化变量;
do{
循环语句;
步进表达式;
}while(比较);
执行流程:
- 初始化变量。
- 执行循环语句、步进表达式。
- 进行比较,若为true,则执行循环表达式、步进表达式并再次进行比较;若为false,则跳出循环。
与普通while循环不同,do…whle循环中循环语句与步进表达式至少会执行一次。
package org.example;
public class Main {
public static void main(String[] args) {
do {
System.out.println("Hello World");
}while(false);
}
}
4.8循环控制语句
循环控制语句有break与continue关键字可使用:
- break:
- 在switch中代表结束switch语句。
- 在循环中代表结束循环。
- continue:
- 结束当前本次循环,直接进入下一次循环,直到条件为false为止。
五、数组
5.1引用数据类型
Java包含引用数据类型和基本数据类型两种,其中基本数据类型有八种,其余的包括类、接口、数组和枚举类型都属于引用数据类型。
- 类:用户定义的类型,包括像String这样的内置类。
- 接口:定义了方法的集合,实现它的类需要提供这些方法的具体实现。
- 数组:可以存储多个同类型的元素。
- 枚举:一组限定了的常量。
引用数据类型与基本数据类型的区别在于,基本数据类型所存储的字面量是一个值,而引用数据类型存储的是一个地址/引用,该地址指向引用变量所存储的真正内容(虽然java中并没有指针,但不代表java不使用指针,指针只是被封装在java编译器内部,我们无法窥见而已)。具体而言,引用数据类型与基本数据类型的区别在于:
- 存储位置:基本数据类型存储在栈(stack)中,而引用数据类型在栈中存储的是一个地址,这个地址指向堆(heap)中的实际数据。
- 传递方式:Java是一种值传递语言,对于基本数据类型,传递的是值的副本,而对于引用数据类型,传递的是对象的引用/地址。
- 默认值:基本数据类型的默认值是0或false,而引用数据类型的默认值是null。
基本数据类型默认值:
数据类型 | 默认值 |
---|---|
整数 | 0 |
小数 | 0.0 |
字符类型 | ‘\u000’(ASCII值为0对应的字符) |
布尔 | false |
5.2数组的基本介绍
数组是一个容器,用于一次存储多个数据,数组本身属于引用数据类型,并且有如下特点:
- 既可以存储基本数据类型,也能存储引用数据类型的数据。
- 数组定长,一旦定义就无法更改。
定义方式:包括动态初始化与静态初始化两种定义方式。
- 动态初始化:在定义数组时,没有给具体的数据,只指定了长度。
数据类型[] 数组名=new 数据类型[长度];
数据类型 数组名[]=new 数据类型[长度];
- 静态初始化:在定义数组时,直接给出数据。
数据类型[] 数组名=new 数据类型[]{元素1,元素2,...};
数据类型 数组名[]=new 数据类型[]{元素1,元素2,...};
- 简化的静态初始化:
数据类型[] 数组名={元素1,元素2,...};
动态初始化与静态初始化的区别:
- 动态初始化:定义时,只指定了长度,没有存具体的数据。
- 静态初始化:定义时就已知存什么数据。
注意,静态初始化中的元素可以是变量,但变量必须已赋值。
5.3数组的基本操作
1.获取数组的长度
数组名.length
//注意,length不是数组对象的方法,而是数组的属性
2.数组的索引
元素在数组中存储的位置称为元素在数组中的索引。索引都是从零开始,最大索引为数组的长度减1.
3.存储、获取元素
格式:
//向数组中存储元素
数组名[索引名]=值;
//从数组中获取元素
变量=数组名[索引值];
注意,直接输出数组名,会输出数组在内存中的地址值(数组对象的引用)。并且,若数组中没有存储数据,直接获取也能得到数据(元素的默认值)。
4.数组的遍历
数组的遍历,即将元素从数组中一个个取出。
package org.example;
public class Main {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,9};
for(int i=0;i<arr.length;i++){
System.out.println("arr["+i+"]="+arr[i]);
}
}
}
5.数组的扩容
将数组arr1(地址为0x001)由大小为5扩容为大小为10,可生成一个新的长度为10的数组arr2(地址为0x002),将数组arr1的元素复制到arr2当中,再将arr2所保存的引用传递给arr1进行保存即可。
package org.example;
public class Main {
public static void main(String[] args) {
int[] arr1={1,2,3,4};
int[] arr2=new int[10];
for(int i=0;i<arr1.length;i++){
arr2[i]=arr1[i];
}
arr1=arr2;
System.out.println("arr1的长度为"+arr1.length);
}
}
5.4内存图
1.内存的划分
在Java当中,内存被划分为五部分:
- 栈(stack):函数的运行都会进入栈中,运行完毕之后,需要弹出函数调用栈,释放空间。
- 堆(heap):保存对象、数组,且每一次使用new,都会在堆内存中开辟空间,并为这个空间分配一个地址/引用,注意,堆内存中的数据都是有默认值的。
- 方法区(Method Area):代码的预备区,记录了类的信息以及方法的信息,方法区中主要保存class文件以及其中的信息。代码运行之前,需要先进内存,即存放在内存的方法区中。
- 本地方法栈(Native Method Stack):专门运行native方法(本地方法)。本地方法可理解为堆对Java功能的扩充,即有很多功能Java语言无法实现,就需要本地方法进行完成。如I/O流当中,包含读(将内存当中的数据写入本地硬盘上)和读(将本地硬盘中的数据读到内存当中),这两个操作都涉及底层硬件操作,Java只能通过调用本地方法(封装的系统调用)来实现。
- 寄存器
一个Java程序的运行流程如下:
- 编写Java文件并经过编译得到字节码文件,在运行该字节码文件时需要先将其加载到内存当中(方法区)。
- 程序首先从main方法开始运行,故会在栈中开辟main方法的函数调用栈。
- 由于main方法中使用了new语句,故需要在堆当中申请内存存放数组数据(开始时保存元素默认值),并返回该空间的地址/引用给变量arr。
2.一个数组内存图
- 运行准备过程同上,方法区中保存了程序信息,并在运行main方法时为该方法在栈中开辟空间。
- 在堆中开辟空间的过程同上。
- 在执行
println(arr)
时,会直接输出该数组的地址(引用)。 - 在执行
println(arr[1])
时,会输出对应元素(初始时为默认值)。 - 在执行
arr[1]=100
时,将堆当中对应地址的数据改为100. - 在执行
println(arr[1])
时,会输出对应堆地址当中的元素。
3.两个数组指向同一片空间的内存图
由于arr2不是new得到的,而是arr1直接赋值给的,即arr2存储的地址值与arr1相同,故此时arr1与arr2共用堆内存中的同一片空间(同一个地址值,代表的是同一个数组),此时改变一个数组中的元素会影响到另外一个数组。
5.5二维数组
1.定义
二维数组即是数组中嵌套了多个数组。定义方式为:
- 动态初始化:
数据类型[][] 数组名=new 数据类型[m][n];
数据类型 数组名[][]=new 数据类型[m][n];
数据类型[] 数组名[]=new 数据类型[m][n];
注意,int[][] arr1=new int[2][2];
与int [][] arr2=new int[2][];
的区别如下:
在后者中,两个一维数组未被创建,此时为默认值null(利用此特性可创建不等长的一维数组)。
- 静态初始化:
数据类型[][] 数组名=new 数据类型[][]{{元素1,元素2,...},{元素1,元素2,...},...};
数据类型[] 数组名[]=new 数据类型[][]{{元素1,元素2,...},{元素1,元素2,...},...};
数据类型 数组名[][]=new 数据类型[][]{{元素1,元素2,...},{元素1,元素2,...},...};
在简化的静态初始化可写为:
数据类型[][] 数组名={{元素1,元素2,...},{元素1,元素2,...},...};
数据类型[] 数组名[]={{元素1,元素2,...},{元素1,元素2,...},...};
数据类型 数组名[][]={{元素1,元素2,...},{元素1,元素2,...},...};
2.二维数组的常用操作
package org.example;
public class Main {
public static void main(String[] args) {
String[][] arr={{"a","b","c"},{"d"},{"e","f"}};
//获取二维数组的长度
System.out.println("二维数组长度为:"+arr.length);
//获取二维数组中每个一维数组的长度
for(int i=0;i<arr.length;i++){
System.out.println("第"+(i+1)+"个一维数组的长度为:"+arr[i].length);
}
//二维数组的遍历
for(int i=0;i< arr.length;i++){
for(int j=0;j< arr[i].length;j++){
System.out.print("下标为"+i+","+j+"的元素为"+arr[i][j]+"\n");
}
}
}
}
3.二维数组内存图
- 由于初始时二维数组并未指定三个一维数组的长度,故
arr1[0]
、arr1[1]
、arr1[2]
均为null。 - 在执行
arr1[1]=new int[]{1,2,3};
后,在堆当中为一维数组分配内存,并将其起始地址分配给arr1[1]。 - 在执行
arr1[2]=new int[3];
后,在堆当中为一维数组分配内存,由于一维数组未初始化,故值均为int默认值0,并将起始地址返回给arr1[2]. - 执行
arr1[2][1]=100;
时,格局arr1[2]
存储的0x666找到一维数组的起始地址并修改元素的值。
六、方法
方法是有功能性的代码块,将不同的功能放在不同的方法中,直接调用方法名,对应的方法就可执行,便于维护。方法的通用定义格式为:
修饰符 返回值类型 方法名(参数){
方法体
return 结果
}
通过定义格式,可分为四种类型的方法:
- 无参无返回值方法
- 有参无返回值方法
- 无参有返回值方法
- 有参有返回值方法
方法的通用格式为:
修饰符 返回值类型 方法名(参数){
方法体;
return 结果;
}
- 修饰符:分为访问控制修饰符和非访问控制修饰符,在面向对象中欧给有详细说明,故以下例子中均采用
public void
。 - 返回值类型:方法最终返回结果的数据类型,无返回值时为void。
- 方法名:见名知意,采用小驼峰规范。
- 参数:进入到方法内部参与执行的数据,格式为:
(数据类型1 变量名1,数据类型2 变量名2,...)
。 - 方法体:实现该方法的具体代码。
- return结果:若有返回值,则利用return返回。
形式参数与实际参数的区别:
- 形式参数(形参):在定义方法时形式上定义的参数,此参数还没有值。
- 实际参数(实参):在调用方法时给形参赋予的具体数值。
6.1四类方法
1.无参无返回值方法
public static void 方法名(){
方法体;
}
调用方式:方法名();
注意,void关键字表示无返回值,故在方法中不应写上return。
2.有参无返回值方法
public static void 方法名(数据类型 参数名){
方法体
}
调用方式:方法名(具体参数值);
3.无参有返回值方法
public static 返回值类型 方法名(){
方法体;
return 结果;
}
调用方式:数据类型 变量名=方法名();
4.有参有返回值方法
public static 返回值类型 方法名(数据类型 参数名){
方法体;
return 结果;
}
调用方式:数据类型 变量名=方法名(具体参数值);
6.2数组在方法参数、返回值中的使用
1.数组作为方法参数传递
- main方法进栈运行后执行
int[] arr1
创建数组arr1(数组中的数据保存在堆中)。 - 执行
method(arr1)
调用方法时,为其开辟相应的函数调用栈,并将arr1的起始地址值传递给参数arr2,使其同样指向数组的起始地址。
可见,数组本身是引用数据类型,引用数据类型作方法参数传递时,传递的是地址值。
2.数组作为返回值返回
过程同上。
6.3方法重载
方法重载是指定义多个同名方法,但它们的参数列表不同,使得可以用相同的方法名来执行不同的操作,根据传递给方法的参数类型或数量的不同,编译器会自动选择调用合适的方法。此处的参数列表不同指的是:
- 参数个数不同。
- 参数类型不同。
- 参数类型顺序不同
注意,判断两个方法是否为重载方法,和以下条件无关:
- 和参数名无关。
- 和返回值无关。
会报错:方法调用不明确。
需求:定义三个方法,分别实现2个整数相加、3个整数相加、 4个整数相加。
package org.example;
public class Main {
public static void main(String[] args) {
System.out.println("1+2="+sum(1,2));
System.out.println("1+2+3="+sum(1,2,3));
System.out.println("1+2+3+4="+sum(1,2,3,4));
}
public static int sum(int a,int b){return a+b;}
public static int sum(int a,int b,int c){return a+b+c;}
public static int sum(int a,int b,int c,int d){return a+b+c+d;}
}
七、面向对象
对象,就是对问题中的事物的抽象,而面向对象,就是把现实中的事物都抽象为“对象”。每个对象是唯一的,且都可以拥有它的属性与行为。我们就可以通过调用这些对象的方法、属性去解决问题。
面向对象的三大特性:封装、继承、多态,
- 封装:指在确定系统的某一部分内容时,应考虑到其它部分的信息及联系都在这一部分的内部进行,外部各部分之间的信息联系应尽可能的少。
- 继承:让某个类型的对象获得另一个类型的对象的属性和方法。
- 多态:对于同一个行为,不同的子类对象具有不同的表现形式。
7.1类
类是对一组有相同数据和相同操作的对象的定义,是对象的模板,其包含的方法和数据描述一组对象的共同行为和属性。类是在对象之上的抽象,对象则是类的具体化,是类的实例。类可有其子类,也可有其他类,形成类层次结构。
根据在项目中发挥功能的不同,类可分为以下几种:
- 测试类:带main方法的类,主要用于运行代码。
- 实体类:一类事物的抽象表示。包含以下组成部分:
- 属性(成员变量):描述对象的某一特征,在类内部、方法外部进行定义,作用域为当前类内部。
- 行为(成员方法):描述对象的功能、用途。
package org.pojo;
public class Animal {
//属性(成员变量)
String kind; //品种
int color; //颜色
//行为(成员方法)
public void eat(){
System.out.println("动物要吃饭");
}
public void sleep(){
System.out.println("动物要睡觉");
}
}
7.2对象
对象是类的实体化,对象的使用需要:
-
导包:使用
import 包名.类名
导包,若两个类在同一个软件包下,则此时无需导包即可使用对方的成员。- 特殊包:在使用java.lang下的类时无需导包,如字符串类String。
-
创建对象:需要使用某个类中的成员时,就需要创建该类的对象,基本格式为:
类名 对象名 = new 类名();
- 调用成员(成员变量、成员方法):使用该类的对象调用相应的成员。
7.3匿名对象
常见的变量定义方式有:
//1.基本数据类型的定义
int i=10;
//其中,int是数据类型,i是变量名,等号右边的10是真正的数据。
//2.引用数据类型的定义
Person p=new Person()
//其中,等号右边的new Person()是真正的数据,即为一个Person对象
而所谓的匿名对象,就是没有等号左边的变量定义部分,而只有等号右边的数据对象部分。若只想要定义一个方法并让方法执行,就可考虑使用匿名对象,匿名对象的使用方式为:
new 对象().成员变量/方法;
例:
//Person.java
package org.pojo;
public class Person {
String name;
public void eat(){
System.out.println("人要吃饭");
}
}
//Main.java
package org.example;
import org.pojo.Person;
public class Main {
public static void main(String[] args) {
new Person().eat();
}
}
7.4对象在内存中的划分
- 两个类包括成员变量、方法都会被加载到方法区中(被赋予地址值),main()方法压栈开始执行,创建Phone对象并为其在堆中开辟内存,并将地址返回给对象phone。
- Phone对象在堆中会存储自身类的地址、两个成员变量(初始为默认值),在输出
phone1
、phone1.brand
、phone1.price
时会使出地址以及对应的初始值。 - 在给phone1的成员变量赋值时,jvm会根据对象的地址找到成员变量并赋值。
- 注意,phone1的堆内存当中并不会为call()方法分配存储空间存储,而是当执行clall()方法时会根据所保存的Person类起始值找到方法区中对应的方法并执行。
7.5成员变量和局部变量的区别
- 定义位置不同
- 成员变量:类中方法外。
- 局部变量:在方法内部或参数列表中。
- 初始化值不同
- 成员变量:有默认值,无需赋值就可直接使用。
- 局部变量:无默认值,需先赋值再使用。
- 作用范围不同
- 成员变量:作用于整个类。
- 局部变量:只作用于当前方法内。
- 内存位置不同
- 成员变量:在堆中,虽然类会被加载到方法区中,但赋值、取值操作都是在堆中进行。
- 局部变量:在栈中(此处指的是变量,而非对象)。
- 生命周期不同
- 成员变量:随着对象的创建而产生,随着对象的消失而销毁。
- 局部变量:随着方法的执行而产生,随着方法的结束而销毁。
八、封装
封装,即隐藏对象内部的复杂性,只对外提供公开的接口,便于外界调用,从而提高了系统的可扩展性、可维护性、安全性。通俗来说,将细节隐藏,将对外提供的供别人使用的接口暴露出来,这就是封装的思想。当调用这一接口时,此接口背后封装起来的细节就开始执行,且无需关注细节,只关注公共的接口怎么使用。
8.1private的使用
private修饰的成员只能在本类中使用,在别的类中使用不了。private定义格式:
//修饰成员变量
private 数据类型 变量名;
//修饰成员方法
private 返回值类型 函数名(参数列表){函数体;}
在使用private修饰后,类的成员变量隐藏,但外部也无法获取类的成员信息,此时需对外提供一套公共的使用接口,来对属性进行赋值、取值。
- get()方法:为属性赋值。
- set()方法:获取属性值。
建议使用自动生成的方法模板:
package org.pojo;
public class Person {
private String name;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
8.2this关键字的使用
当局部变量和成员变量重名时,遵循“就近原则”,先访问局部变量,再访问成员变量:
此时,就需要使用this
关键字来区分。
this
代表的是当前对象,用于区分重名的成员变量和局部变量,使用以下可是来访问当前对象成员变量:
this.成员变量名
注意,哪个对象调用的this所在的方法,this就代表哪个对象:
person1调用speak()方法时,this在speak()方法中,故this相当于person1对象,而person2调用speak()方法时同理。
8.3构造方法的定义和使用
在创建person变量时使用new语句生成对象,实际上是调用了Person类的构造方法(方法名与类名相同),该方法生成了一个Person对象。构造方法是指方法名与类名一致,并且能初始化对象的方法,可分为:
- 无参构造:构造对象时无需写入参数。
- 有参构造:构造对象时需要写入参数。
注意,构造方法都是没有返回值的,在其内部不能有return语句,也不能使用void进行修饰。同样可使用idea自动生成构造函数:
package org.pojo;
public class Person {
private String name;
private int age;
//无参构造:直接创建对象
public Person() {
}
//有参构造:创建对象的同时为属性赋值
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
对于无参构造,每一个类都有一个默认的无参构造方法,即使未在类中写出,虚拟机在创建对象时也会自动提供。对于有参构造,虚拟机并不会自动提供,但当类中实现了有参构造后,创建对象时虚拟机会自动调用相应的有参构造创建对象。
8.4JavaBean的使用
JavaBean是Java语言编写的一种标准规范,符合JavaBean的类要求:
- 类必须是具体的(非抽象abstract)和公共的(public)类。
- 具有无参构造和有参构造方法。
- 成员变量私有化,并提供用来操作成员变量的set()和get()方法。
在项目中划分出以下软件包:
- controller(表现层):存放页面相关的类。
- service(业务层):存放页面处理的类。
- dao(持久层):存放和数据库相关的类。
- pojo:存放JavaBean类。
- utils:存放工具类。
标准JavaBean范例(可使用idea快速生成):
package org.pojo;
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
8.5static关键字
1.介绍
static关键字用于修饰静态对象、静态方法,定义方式为:
//修饰一个成员变量
static 数据类型 变量名;
//修饰一个方法
修饰符 static 返回值类型 方法名(参数列表){
方法体;
}
由static修饰的成员并不属于对象,而是属于类。上文Person类中的成员变量name、age都属于对象,必须先创建对象才能进行赋值,且该值只属于该对象,与其他对象无关。而静态成员属于类成员,不属于对象成员,使用时无需创建对象,直接通过:
类名.成员名;
进行调用即可。事实上,静态成员随着类的加载而加载,而非静态成员只有创建对象时才能获得存储空间。并且,由于其属于类,所以凡是根据静态成员所在类创建出来的对象,都快共享这个静态成员。
注意,在生成JavaBean的构造函数时,是自动不带有静态成员变量的。
2.使用
Student类是包含name(private String)、age(private int)和classRoom(public static String)的标准JavaBean。
package org.example;
import org.pojo.Student;
public class Main {
public static void main(String[] args) {
Student.classRoom="123";
Student student1=new Student("小明",18);
Student student2=new Student("小红",19);
}
}
3.static变量的内存说明
静态变量存放在静态域当中,从JDK7开始,静态域设置在堆内存当中。
- main()方法开始运行时,其所在的类加载进方法区当中。
- 当执行静态成员相关语句
Student.classRoom="222"
时,其所在的类就被加载到方法区当中,之后静态成员被加载到堆的静态区中,并被赋值。 - 当初始化变量student1时,会在堆中为其成员变量开辟空间,但在使用静态成员变量时直接在静态区中访问,同理,变量student2与student1共享静态区中的成员变量classRoom。
4.static成员的访问特点
- 静态方法不能直接访问非静态成员:静态方法随着类的加载而加载,而非静态成员随着对象的加载而加载,即静态方法的加载先于非静态成员,故不可直接访问。
- 非静态方法能直接访问静态成员:非静态方法随着对象的加载而加载,而静态成员随着类的加载而加载,即非静态方法的加载慢于非静态成员,故可直接访问。若同属一个类,则直接调用即可,不同类时,可通过类名调用。
- 静态方法能直接访问静态成员:静态方法与静态成员同时和类一起加载进静态区,故可直接访问。当同属一个类时可直接调用,不同类时,可通过类名调用。
- 非静态方法能直接访问非静态成员:非静态方法与非静态成员同时随对象的加载而加载(前者在方法区,后者在堆),故可直接访问。当同属一个类时可直接调用,不同类时,可先创建对象,再通过对象名调用。
8.6可变参数
定义格式:
数据类型...变量名
注意:
- 可变参数的本质是一个数组。
- 参数位置不能连续写多个可变参数,且当可变参数与其他普通参数一起使用时, 可变参数需要放到参数列表最后,以避免传参错误。
例:
package org.example;
public class Main {
public static void main(String[] args) {
sum(1,2,3,4,5);
}
public static void sum(int... arr){
int sum = 0;
for(int i=0;i<arr.length;i++){
sum+=arr[i];
}
System.out.println(sum);
}
}
事实上,可变参数使用数组实现的,可以使用反编译工具进行查看:
8.7对象数组的内存分析
- 执行
Person[] arr=new Person[3];
时,在栈中创建变量arr,并在堆当中申请三个Person对象大小的存储空间,再将空间起始地址返回给arr存储。 - 在堆中依次创建三个Person对象 ,并将地址返回给栈中的Person变量。
- 将三个对象保存到数组中时,会将三个Person变量所保存的Person对象地址值赋值给数组元素(也是Person变量)存储。
九、方法参数与命令行参数
9.1基本数据类型做参数传递
前文介绍过,八种基本数据类型包括整型(byte、shor、int、long)、浮点型(float、double)、字符型(char)、布尔型(bool),除此之外,都为引用数据类型。基本数据类型做参数传递时有如下特点:
- main()方法开始执行时,先将main()方法压入函数栈底,并定义了两个局部变量a、b,由于a、b是基本数据类型,故二者保存在栈中。
- 调用method()方法时,method()方法压栈开始执行,并通过值传递,将main()方法中a、b的值传递给method()方法参数列表中method()方法的局部变量a、b(虽同名,但属于不同方法的局部变量,且由于局部变量仅作用于当前方法,故在method()方法中修改的是method()方法的局部变量,而对main()方法中的局部变量无影响)。
- method()方法运行完时弹出函数调用栈并清空所占内存,继续执行main方法,此时输出的a、b是main的局部变量a、b,其值仍为10、20.
9.2引用数据类型做参数传递
引用数据类型仍是值传递,但传递的是对象的地址。
- 执行main()方法时,先创建数组变量arr,并为其两个整型对象在堆内存中开辟空间,将地址返回给arr保存。
- method()方法压栈执行,将main()方法的局部变量arr保存的地址值作为参数传递给method()方法中的同名局部变量arr,此时两个arr指向同一个数组。在执行method()方法时,完成对堆中数据的修改。
- 返回到main()方法时,method()已出栈,但数组元素已被修改,故输出20,40.
9.3命令行参数
使用命令行运行Java程序时仍可传递参数,此时参数被保存在字符串数组当中:
事实上,可在配置
当中进行编辑:
十、继承
在生产过程中往往会定义多个类,这些类可能会存在大量重复性的代码,此时可定义一个父类,将相同的代码抽取到父类中,其他类直接继承这个父类时就可以使用这个父类中的代码。Java中提供了关键字extends
来实现继承:
子类 extends 父类
注意,子类可以继承父类中私有和非私有成员,但不能使用父类中的私有成员,并且父类的构造方法不能继承。
10.1继承的基本使用
继承的使用流程:
- 定义一个父类,在其中定义重复性的代码。
- 定义一个子类继承父类。
- 创建子类对象,直接使用父类中非私有成员。
10.2成员变量的访问特点
- 父类对象只能调用父类中的成员变量。
- 子类对象可以调用子类中的成员变量与父类中非私有成员变量。
- 口诀:等号左边的变量类型决定优先调用父/子类的成员,子类当中没有时会去父类中寻找。
10.2.1子类与父类成员变量不重名
10.2.2子类与父类成员变量重名
10.2.3父类变量赋值子类对象、子类变量赋值父类对象
10.3成员方法的访问特点
- 父类对象只能调用父类中的成员方法。
- 子类对象可以调用子类中的成员方法与父类中非私有成员方法。
- 口诀:new后接的对象类型决定调用父/子类的方法,子类当中没有时会去父类中寻找。
10.3.1子类与父类成员方法不重名
10.3.2子类与父类成员方法重名
10.3.3父类变量赋值子类对象、子类变量赋值父类对象
10.4方法的重写
当子类中有和父类方法名以及参数列表相同的方法的时,就是对父类方法的重写。在使用该重名方法时,要看new的对象类型,若是子类,则调用子类中的同名方法,否则调用父类中的同名方法。
10.4.1检测是否为重写方法
在方法上写注解@Override
,不报错即为重写方法。
10.4.2重写方法的注意事项
- 子类重写父类方法之后,权限必须保证大于等于父类权限(权限指访问权限),而Java中权限由访问修饰符决定,修饰符权限由小到大依次为:public、protected、默认(不写就是默认)、private。
- 子类方法重写父类方法,方法名和参数列表要相同。
- 父类的私有方法、构造方法、静态方法子类不能重写。
- 子类重写父类方法后,返回值类型应当是父类方法返回值类型的子类类型。
10.5构造方法
10.5.1子类构造方法与父类构造方法的联系
创建子类对象时会先初始化父类(先执行父类的无参构造),这是因为每个构造方法的第一行都默认有一个super();
,表示执行父类的无参构造,即使不写虚拟机也会自动提供。
父类无参构造+子类无参构造
父类无参构造+子类有参构造
10.5.2super与this的具体使用
super的使用
super代表父类引用,有以下用途(只能在子类的构造当中使用):
- 可以用来调用父类构造方法。
super();
- 可以用来调用父类成员变量、成员方法。
super.父类成员方法名(实参列表);
super.父类成员变量名();
this的使用
this代表的是当前对象(哪个对象调用的this所在的方法,this就代表哪个对象),可用于区分重名的成员变量与局部变量、调用当前对象中的成员。常见使用方式为:
- 调用当前对象的构造方法。
this()
:调用当前对象的无参构造。this(实参)
:调用当前对象的有参构造。
- 调用当前变量的成员变量、成员方法。
this.成员变量名
。this.成员方法名(实参)
。
注意,super与this在构造当中使用时都必须在第一行,所以二者不能同时写在同一个构造方法中。
10.6继承的特点
- 继承只支持单继承,不支持多继承,即一个类只能继承于一个父类,不能有多个父类。
- 继承支持多层继承,即A类继承于B类,B类可继承于C类(一脉单传),此时A类也可使用C类当中的非私有成员。
- 一个父类可有多个子类。
- 父类中的方法:
- 构造方法不能继承,也不能重写。
- 私有方法可以继承,但不能重写。
- 静态方法可以继承,但不能重写。
十一、抽象、接口
11.1抽象
11.1.1抽象类与抽象方法
父类中的方法,被他的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法,将包含了抽象方法的类称为抽象类。抽象类与抽象方法的定义方式为:
//定义抽象方法
修饰符 abstract 返回值类型 方法名(参数列表);
//抽象类
public abstract class 类名{}
总结:
- 抽象方法所在的类一定是抽象类。
- 抽象类中不一定含有抽象方法。
- 可使用注解
@Override
来标注当前方法是抽象方法的实现方法。
当子类继承抽象父类时,需要重写父类中所有的抽象方法,否则报错(或者将子类也声明为抽象类)。并且,由于抽象类并不具体,所以抽象类不能用于创建对象,即不能实例化,只能通过创建子类对象来调用重写的方法。事实上,可将抽象类看作是一类事物的标准,要求只要是属于这一类的,就必须拥有抽象类中的方法,而这些方法如何实现,就由子类的具体用途决定。
11.1.2注意事项
- 抽象类不能直接用于创建对象(即使不含有抽象方法,也不能实例化),只能创建非抽象子类的对象。
- 抽象类中不一定得有抽象方法,但抽象方法一定在抽象类中。
- 抽象类的子类必须重写父类中的所有抽象方法,或也声明为抽象类,否则报错。
- 抽象类中可以有成员变量、构造方法(供子类创建对象使用,因为子类构造方法中默认有
super()
)、成员方法。
11.2接口
接口是一个引用数据类型,是一种标准、规则,可以理解为一种特殊的类,里面全部是由全局常量和公共的抽象方法所组成。接口是解决Java无法使用多继承的一种手段,但是接口在实际中更多的作用是制定标准的。或者我们可以直接把接口理解为100%的抽象类,既接口中的方法必须全部是抽象方法。
特点:
- 一个接口也能够拥有方法和属性,但是在接口中声明的方法默认是抽象的,即只有方法标识符,而没有方法体。
- 如果一个类实现了一个接口中要求的所有的方法,然而没有提供方法体而仅仅只有方法标识,那么这个类一定是一个抽象类。
- 一个接口就是描述一种能力,所以接口的作用就是告诉类,你要实现我这种接口代表的功能,你就必须实现某些方法,我才能承认你确实拥有该接口代表的某种能力。
在JDK7及以前的版本中,接口可以有以下成员:
- 抽象方法:
public abstract 返回值类型 方法名(参数列表i);
,即使不写public abstract
也是默认带有。 - 成员变量:
public static final 数据类型 变量名=值;
,即使不写public static final
也是默认带有。其中,final
关键字表示最终的,被其修饰的变量不能二次赋值,所以一般将final
修饰的变量视为常量。
在JDK8中,接口当中可以加入默认方法与静态方法:
- 默认方法:
public default 返回值类型 方法名(形参){方法体}
,默认方法带有方法体,是可以实现的,其中public即使不写也会带有。 - 静态方法:
public static 返回值类型 方法名(形参){方法体}
,可通过接口名.方法名()
进行调用,并且,public即使不写也会带有。
在JDK9中,接口当中可以加入私有方法private方法。
11.2.1接口的基本使用
接口的定义与实现:
//定义接口
public interface 接口名{}
//实现接口
public class 实现类名 implements 接口名{}
接口的使用:
- 实现类实现接口,需要重写接口中的抽象方法。
- 创建实现类对象(接口不能直接创建对象)。
- 调用重写的方法。
11.2.2接口中的方法
1.抽象方法
public abstract 返回值类型 方法名(参数);
- 不写
public abstract
也默认带有。 - 在使用时应先定义实现类,在类中重写抽象方法,创建实现类对象,才可调用重写的方法。
2.默认方法
默认方法与静态方法是JDK8的新特性。
public default 返回值类型 方法名(参数){
方法体;
}
需要先定义实现类,实现接口,默认方法可重写可不重写,使用实现类创建对象后进行调用即可,并且,public即使不写也会带有。
3.静态方法
默认方法与静态方法是JDK8的新特性。
public static 返回值类型 方法名(参数){
方法体;
}
使用接口名即可直接调用,无需实现该接口,并且,public即使不写也会带有。
11.2.3接口中的变量
变量的定义格式为:
public static final 数据类型 变量名=值;
即使不写上public static final
也会默认带有。其中,final
代表最终的,被它修饰的变量不能二次赋值,可视为常量。在使用接口中变量时,通过接口名直接进行调用即可。
注意,由final
、final static
修饰时有如下特点:
其中,使用final static
修饰的变量名一般全部大写,将其当作常量进行使用。
11.3接口的特点以及和抽象类的区别
11.3.1接口的特点
- 接口可以多继承,即一个接口可以继承多个接口。
public interface Interface_A extends Interface_B,Interface_C{}
- 接口可以多实现,即实现类可以实现一个或多个接口。
public class Class_A implements Interface_B,Interface_C{}
- 一个子类可以继承一个父类的同时实现一个或多个接口。
public class Class_A extends Class_B implements Interface_C,Interface_D{}
注意:
- 不管是继承父类还是实现接口,只要是父类中或者接口的抽象方法,子类或实现类都需要重写。并且,当一个类实现多个接口时,若接口中的抽象方法有重名且参数一样,则只需重写一次。
2. 当一个类实现多个接口时,若默认方法有重名的,且参数列表一样,则必须重写一次默认方法。
11.3.2接口和抽象类的区别
相同点
- 都位于继承体系的顶端,用于被其他类实现或继承。
- 都不能使用new创建对象。
- 都包含抽象方法,其子类或者实现类都必须重写这些抽象方法。
不同点
- 抽象类:一般作为父类使用,可以有成员变量、构造器、成员方法、抽象方法等。
- 接口:成员单一,一般抽取接口时,抽取的都是方法。
- 类不能多继承,但是接口可以。
十二、多态
11.1多态的介绍与基本使用
Java的多态特性允许不同类型的对象对同一方法进行不同的实现,即方法的重写。具体来说,多态性指的是通过父类变量来引用子类的对象,从而实现对不同对象的统一操作。出现多态的前提是:
- 继承关系:存在继承关系的父子类之间才能使用多态性。
- 方法重写:子类必须重写(Override)父类的方法。通过方法的重写,使得可以根据子类的特点行为改变原方法的功能。
- 父类变量保存子类对象:使用父类变量来存储子类对象。这样可以实现对不同类型的对象的统一操作,而具体调用哪个子类的方法会在运行时多态决定
FatherClass fatherclass=new SonClass();
在这个示例中,Animal 类是父类,Dog 和 Cat 类是它的子类。通过将父类变量分别指向子类对象,实现了多态性。在运行时,根据对象的实际类型来调用相应的子类方法。
11.2多态的访问特点
- 多态情况下,子类和父类存在同名的成员变量时,访问的是父类的成员变量。
- 多态情况下,子父类存在同名的非静态成员方法时,访问的是子类中重写的方法。
- 多态情况下,子父类存在同名的静态成员变量、成员方法时,访问的是父类的成员函数。
- 多态情况下,不能访问子类独由的方法。
两句口诀:
- 访问同名成员变量时,访问的是等号左边的(父类)成员变量。
- 访问同名成员方法时,若是静态的,则访问等号左边(父类)的成员函数,否则访问等号右边的(子类)成员函数。
11.3多态的好处
使用原始方式new出来的对象(左右两边类型相等),既能调用重写的,也能调用继承的,还能调用自己特有的成员,但是使用多态方式new出的对象,只能调用重写的,不能直接调用子类特有的成员。但是,这种方式能够调用父类非私有成员、父类特有成员。
可见,形参传递父类类型,调用此方法父类类型就可以接收任意它的子类对象。传递哪个子类对象,就指向哪个子类对象,就调用哪个子类对象重写的方法。
原始方式:
- 优点:既能调用重写的,也能调用父类非私有的,还能调用自己特有的。
- 缺点:扩展性差。
多态方式
- 优点:扩展性强。
- 缺点:不能直接调用子类特有功能
11.4多态中的转型
在上文的例子当中,使用Animal变量指向对象dog时无法访问子类Dog所独有的方法,这使得程序的编写并不灵活。事实上,可利用向上、向下转型进行改善。
11.4.1向上转型
上文中,将一个子类的对象引用赋值给其父类类型变量,此时,子类对象可以被视为父类对象,使得可以以,统一的方式处理不同类型的对象,实现代码的灵活性和可扩展性,这就是向上转型。向上转型的特点如下:
- 子类对象可以隐式地转型为父类对象(即被赋值给父类变量),以父类类型的变量来引用子类对象,不需要任何显式的类型转换操作。
- 父类变量可以引用子类对象,但无法访问子类独有的成员,并且,子类对象中重写的方法,在通过父类变量调用时,会调用子类中的实现。
- 向上转型是安全的操作,因为子类对象本身就是一个父类对象。
转换方式:
父类类型 对象名=new 子类对象();
最常见的一种向上转型:
double b=1; //将整型值赋值给double型变量,实现隐式类型转换
11.4.2向下转型
向下转型将一个父类类型的变量转换为子类类型的变量。它与向上转型相反,需要进行显式的类型转换操作。在向上转型当中,对象被向上转型后,它的具体类型信息会丢失,只保留了父类类型的信息,但在向下转型后,就可访问子类中特有的功能。注意,向下转型是有风险的,因为转换的对象必须是当前对象的真实子类才能成功,否则会在运行时抛出ClassCastException
异常。
转换方式:
父类类型 变量名1=new 子类对象(); //向上转型
子类类型 变量名2=(子类类型)变量名1; //向下转型
上文中代码可改为:
package org.example;
import org.pojo.Animal;
import org.pojo.Cat;
import org.pojo.Dog;
public class Main {
public static void main(String[] args) {
Animal animal = new Dog();
method(animal); //Dog eat
animal=new Cat();
method(animal); //Cat eat
}
public static void method(Animal animal){
animal.eat();
if (animal instanceof Dog){
Dog dog = (Dog) animal;
dog.lookDoor();
}else {
Cat cat = (Cat) animal;
cat.catchMouse();
}
}
}
11.4.3instanceof关键字
在转型过程中,由于Dog类和Cat类都是Animal类的子类 ,所以在将子类Cat的对象变成了兄弟类的对象就不是向下转型,因此会报类型转换异常。ava为我们提供了一个关键字instanceof,它可以帮助我们避免出现ClassCastException这样的异常。
变量名 instanceof 数据类型
- 如果变量属于该数据类型或者其子类型,返回true。
- 如果变量不属于该数据类或者其子类型,返回false
package org.example;
import org.pojo.Animal;
import org.pojo.Cat;
import org.pojo.Dog;
public class Main {
public static void main(String[] args) {
//向上转型
Animal animal = new Dog();
//先判断animal指向的对象类型再向下转型
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.lookDoor();
}else {
Cat cat = (Cat) animal;
cat.catchMouse();
}
}
}
11.5权限修饰符
在java中提供了四种访问权限,使用不同的访问权限修饰符时,被修饰的内容会有不同的访问权限。
- public:公有的,时最高权限,被public修饰的成员在哪里都能访问。
- protected:受保护的。
- default:默认的,不写权限修饰符时就是默认权限(在接口中定义方法需要使用default)。
- private:私有的,只能在自己的类中访问。
public | protected | default | private | |
---|---|---|---|---|
同类 | √ | √ | √ | √ |
同包不同类 | √ | √ | √ | × |
不同包,但是父子类 | √ | √ | × | × |
不同包,不是父子类 | √ | × | × | × |
同一个类下
同包不同类
不同包,但是父子类
不同包,且不是父子类
对于protected,在不同包但是父子类
的情况下,不使用super
进行调用父类成员,而使用子类对象进行调用时:
这是因为myclass.name
这种调用方式属于子类对象调用而不是子类调用,
11.6final关键字
final是java语言中的一个关键字,表示最终的、不可变的,可以修饰变量以及方法,还有类等。被修饰后的类、方法、变量将被赋予最终
的含义:
- 类:修饰后的类不能被子类继承,格式为:
public final class 类名{}
- 方法:被修饰后的方法不能被重写,格式为:
修饰符 final 返回值类型 方法名(形参){
方法体;
}
注意,final
与abstract
不能同时使用。
- 变量:在Java中有局部变量、成员变量,其中成员变量包含实例成员变量与静态成员变量,根据变量类型不同,final所起作用也不同。
1.final修饰局部变量
在函数体中使用final定义变量时,不能对变量二次赋值:
在定义的函数中将final修饰的变量作为参数进行传递时,在函数体内部不能二次赋值:
2.final修饰成员变量
注意:
- final修饰的变量是基本类型时,那么变量存储的数据值不能发生改变。
- final修饰的变量是引用类型时,那么变量存储的地址值不能发生改变,但是地址指向的对象内容是可以发生变化的。
十三、代码块
13.1构造代码块
格式:
{
代码
}
执行特点:
- 构造代码块优先于构造方法执行,每创建一次对象,就会执行一次。
13.2静态代码块
格式:
static{
代码
}
执行特点:
- 优先于构造代码块、构造方法执行,且只执行一次。
静态代码块的使用场景:若想让一些数据最先初始化,而且只需要初始化一次,就可以将这些数据放到静态代码块中。较常见的是在操作数据库的过程中。
Java代码中操作数据库往往包含注册驱动
、初始化操作数据库的地址
、初始化数据库用户名
、初始化数据库密码
,为避免每操作数据库就初始化一次,可将参数的初始化操作放在静态代码块当中。
十四、内部类的使用
当一个事物的内部还有一个部分需要完整的结构去描述,且该结构又只为外部事物提供服务,则该结构最好使用内部类。在Java中运行一个类定义于另一个类的内部,前者称为内部类,后者称为外部类。如:
class A{
class B{
}
}
类A是类B的外部类,类B是类A的内部类。内部类可分为成员内部类(静态、非静态)、局部内部类、匿名内部类。
14.1静态成员内部类
静态成员内部类直接在定义内部类时加上static
关键字,语法格式为:
public class A{
static class B{
}
}
- 内部类可以定义属性、方法、构造。
- 静态内部类可以被final、abstract修饰(不能同时修饰),其中被final修饰后不能被继承,被abstract修饰后需要被实现才能创建对象。
- 静态内部类不能调用外部类的非静态成员。
- 内部类可以被四种权限修饰符修饰。
静态内部类成员的调用方式为:
外部类.内部类 对象名 = new 外部类.内部类();
14.2非静态成员内部类
语法格式为:
public class A{
class B{
}
}
调用方式为:
外部类.内部类 对象名 = new 外部类().new 内部类();
外部类的成员变量和内部类的成员变量、内部类的局部变量重名时的区分方式
package org.pojo;
public class Person {
String name="张三"; //外部类的成员变量
public class Internal{
String name="李四"; //内部类的成员变量
public void method(String name){ //内部类的局部变量
System.out.println(name); //调用内部类的局部变量
System.out.println(this.name); //调用内部类的成员变量
System.out.println(Person.this.name); //调用外部类的成员变量
}
}
}
14.3局部内部类
14.3.1基本使用
局部内部类可以定义在方法、构造、代码块中。
14.3.2局部内部类的实际操作
14.3.2.1接口类型作为方法参数传递和返回
- 接口作为方法参数,传递实参时,传递的是实现类对象。
- 接口作为方法返回值返回时,返回的是实现类对象。
14.3.2.2抽象类作为方法参数传递和返回
- 抽象类作为方法参数传递时,传递的是其子类对象。
- 抽象类作为方法返回值传递时,返回的是其子类对象。
14.3.2.3普通类作为方法参数传递和返回
- 普通类作为方法参数传递时,传递的是对象。
- 普通类最为方法返回值返回,返回的是对象。
14.3.2.4局部内部类的实际操作
14.4匿名内部类
匿名内部类即没有显示声明类名的内部类。使用格式:
//1.匿名对象的使用方式
new 接口/抽象类(){
重写方法;
}.重写的方法();
//2.非匿名对象的使用方式
类名 变量名=new 接口/抽象类(){
重写方法;
}
变量名.重写的方法();
问题描述:想要实现接口,简单使用一次抽象方法,就需要创建一个实现类、实现这个接口、重写抽象方法、new类对象,此时可以使用匿名内部类简化代码。
14.4.1匿名内部类的应用:当参数传递
14.4.2匿名内部类的应用:当返回值返回
十五、异常
15.1异常介绍及出现过程
1.异常的介绍
异常是指程序代码中出现了不正常的现象,在Java中,异常被定义成了不同的类。在Java API文档中搜索类Throwable类:
Java根据错误的性质将运行错误分为错误和异常:
- 错误(Error):包括一些严重的程序不能处理的系统错误类,如内存溢出、虚拟机错误、栈溢出等。这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。常见的error有:NotClassDeFountError(类未定义错误),OutofMemoryError(内存溢出错误),StackOverFlowError(栈溢出错误)。
- 异常(Exception):程序错误引起的异常事件,包括运行时异常和检查型异常(语法问题都不是异常)。
- 运行时异常:运行时出现异常,RuntimeException及其子类都属于运行时异常,这些异常程序员不需要去手动的catch。当程序中出现运行时异常时,java虚拟机会进行处理。常见的运行时异常有:空指针异常,算术异常,数组越界异常等。
- 编译时异常:代码编译后出现的异常(语法错误除外),必须得进行捕获,否则编译不过去。事实上,Exception及其子类(除了RuntimeException外)本身就属于编译时异常,常见的编译时异常有IOException,SqlException。
注意,Error是系统错误或资源错误引起,是不可控的,而Exception是程序错误引起的,是可控的。在Java中,所有的异常都被定义成了Throwable的子类或者子类的子类。
2.异常的出现过程
- 在执行
System.out.println(arr[6]);
时出现了ArrayIndexOutOfBoundsException
异常,jvm在接收到此异常时将异常对象创建出来并检查代码,发现没有处理异常的代码,之后默认将该异常向上抛(抛给调用异常所在方法的方法),抛给了main()。 - 同理,main()方法没有处理异常的函数,继续向上抛,抛给了调用main()的jvm,此时jvm只能自己处理该异常,有以下默认处理方式:
- 将异常信息打印到控制台。
- 终止程序的执行。
15.2异常的处理方式
15.2.1throw与throws
在Java当中提供了两种异常抛出关键字throw
和throws
,其中,throw关键字用于手动抛出异常对象,可在函数体中使用,而throws关键字用于声明可能会抛出哪些异常(但实际并未抛出)并在函数调用栈中将此异常向上抛出,在函数签名中使用,语法格式:
//1.throw
throw new 异常名称(构造函数参数);
//2.throws
函数修饰符 返回值类型 函数名(参数列表)throws 异常列表{
函数体;
}
可见,虽然使用throws
关键一直将异常上抛可使程序通过编译并运行,但在出现异常时jvm仍会终止程序的运行。
throws可同时抛出多种异常,格式为:
throws 异常1,异常2,...
例:
public static void add(String str)throws FileNotFoundException,IOException {
if(str==null)throw new IOException("IO异常");
if(!str.endsWith(".txt")){
//手动抛出异常
throw new FileNotFoundException("文件找不到");
}
System.out.println("add()");
}
事实上,若throws的多个异常之间有字符类继承关系时,可直接throws父类异常(可直接使用Exception
抛出所有异常)。上述代码中FileNotFoundException
是IOException
的子类,故可写为:
public static void add(String str)throws IOException {
if(str==null)throw new IOException("IO异常");
if(!str.endsWith(".txt")){
//手动抛出异常
throw new FileNotFoundException("文件找不到");
}
System.out.println("add()");
}
15.2.2try…catch
1.抓取单个异常
Java提供了try...catch
关键字对异常进行捕获处理,语法格式为:
try{
可能出现异常的代码;
}catch(异常 对象名){
处理异常的代码;
}
与throws
方法不同的是,函数add()抛出异常后main()函数仍可继续执行。但若是catch
并未捕获到try
中所抛出的异常,程序仍会中断执行(虚拟机抓到异常后默认终止程序的执行):
若想抓到所有的异常,也可使用catch抓取Exception
。
2.抓取多个异常
try{
可能出现异常的代码;
}catch(异常1 对象名1){
处理异常的代码;
}catch(异常2 对象名2){
处理异常的代码;
}catch(异常3 对象名3){
处理异常的代码;
}
例:
package org.example;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Main {
public static void main(String[] args){
String str="a.txt1";
try{
add(str);
}catch (FileNotFoundException e){
System.out.println(e);
}catch (IOException e){
System.out.println(e);
}
System.out.println("继续执行");
}
private static void add(String str)throws FileNotFoundException,IOException{
if(str==null)throw new IOException("IO异常");
if(!str.endsWith(".txt")){
//手动抛出异常
throw new FileNotFoundException("文件找不到");
}
System.out.println("add()");
}
}
注意,若catch
的多个异常之间有子父类继承关系,则可直接catch
父类异常。
15.2.3finally关键字
finally
代表不管是否触发了异常,都会执行的代码块。特殊情况:执行了System.exit(0)
,则会终止执行正在执行的Java程序,此时JVM会被终止。一般配合try...catch
使用该关键字:
try{
可能出现异常的代码;
}catch(异常 对象名){
处理异常的代码;
}finally{
不管是否有异常都会执行的代码
}
例:
package org.example;
import java.io.FileNotFoundException;
public class Main {
public static void main(String[] args){
String str="a.txt1";
try{
add(str);
}catch (FileNotFoundException e){
e.printStackTrace();
}finally {
System.out.println("finally代码块");
}
System.out.println("继续执行");
}
private static void add(String str)throws FileNotFoundException{
if(!str.endsWith(".txt")){
//手动抛出异常
throw new FileNotFoundException("文件找不到");
}
System.out.println("add()");
}
}
注意,即使在执行finally
语句之前使用return
语句进行返回,finally
语句仍会执行:
事实上,在执行catch中的return语句时会先检查是否存在finally代码块,若存在,则先执行完finally代码块再执行catch中的return语句。而若finally代码块中也含有return语句,则不会再回到catch中的return语句,而是使用finally中的return语句结束函数的执行。
事实上,finally代码块中常存放释放资源的代码,有的对象在使用完后可由GC(垃圾回收器)自动回收以释放内存,而有的对象GC无法释放,如数据库连接对象Connection、IO流对象、Socket对象,此时就需要在finally代码块中手动释放。
15.2.4继承中方法重写抛异常问题
1.父类中的方法抛出了异常,
父类中抛出异常时,子类可抛可不抛。
2.父类中的方法没有抛出异常,
父类中未抛出异常时,子类可不抛。
15.3throws和try…catch的使用时机
若处理异常之后,还想让后续的代码正常执行,就可使用try...catch
,而若方法之间是递进关系(调用与被调用关系),则可先throws
,但到最后仍需要使用try...catch
作异常处理。举例:
若dao层出现异常,则可将异常抛出给Service层,Service层再抛出给Controller层,在该层中对异常进行处理。
15.4自定义异常
15.4.1自定义异常
需求:键盘登入一个用户,实现登录功能,如果登录失败,抛出LoginUserException异常(自定义异常)。
15.4.2打印异常信息的三种方式
Throwable
类中的方法:
- String toString():输出异常类型和设置的异常信息。
- String getMessage():输出设置的异常信息。
- void printStackTrace():打印所有信息,包含异常类型、异常信息、出现的行数等。
十六、Object类
Object
类(java.lang
包下)是所有类的父类,其他类直接或间接都是Object
类的子类。
16.1toString方法
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
toString()方法返回该对象的字符串表示形式。创建标准的JavaBean类Person,包含name(String)、age(int)属性,当不重写toString()方法时执行:
package org.example;
import org.pojo.Person;
public class Main {
public static void main(String[] args) {
Person person = new Person("Mike",18);
System.out.println(person); //org.pojo.Person@10f87f48
System.out.println(person.toString()); //org.pojo.Person@10f87f48
}
}
输出返回Person类的路径以及对象的引用(Integer.toHexString(hashCode()
),该格式即为Object类中toString方法的输出格式。有:
- 若没有重写Object中的toString方法,直接输出对象名会默认调用Object中的toString方法,直接输出地址值。
- 若重写了Object中的toString方法,可将对象内容作为返回值输出。
重写Person类中的toString方法:
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
此时输出:
16.2equals方法
equals
方法用于比较两个对象的地址值是否相等,查看Object类中的equals
方法:
public boolean equals(Object obj) {
return (this == obj);
}
例:
package org.example;
import org.pojo.Person;
public class Main {
public static void main(String[] args) {
Person person1 = new Person("Mike",18);
Person person2 = new Person("Mike",18);
System.out.println(person1==person2); //false
System.out.println(person1.equals(person2)); //false
}
}
注意,==
针对基本数据类型时(如整型、字符串),比较的是值,而针对引用数据类型时,比较的是地址值,这是无法重写的。查看String当中的equals方法,即可发现该方法已被重写:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
return (anObject instanceof String aString)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value);
}
重写Person类中的equals方法即可:
@Override
public boolean equals(Object obj) {
//1.先判断传递的是否是对象本身(提高效率)
if(obj==this)return true;
//2.先判断obj是否为null
if(obj==null)return false;
//1.先判断类型,避免类型转换出错
if(obj instanceof Person){
//4.obj接收了子类Person对象,在多态的条件下不能直接调用子类的特有属性name、age,故需要使用向下转型
Person p=(Person)obj;
return this.name.equals(p.name)&&this.age==p.age;
}
return false;
}
可直接使用Alt+Insert
生成equals方法:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
16.3clone方法
clone方法用于复制一个属性值一样的新对象,使用时需要被克隆的对象实现Cloneable
,并重写clone方法。实现Cloneable
:
public class 类名 implements Cloneable{
}
//在类中重写clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
例:
package org.example;
import org.pojo.Person;
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person("Mike",18);
Object o=person1.clone(); //将异常抛出
Person person2 = (Person)o; //克隆一个新对象
System.out.println(person1==person2); //false,person2与person1地址值不同,是新对象
System.out.println(person1.equals(person2)); //true,person1与person2各属性相同
}
}
16.4经典接口说明
16.4.1Comparable接口
基本数据类型(除boolean外)需要比较大小时,直接使用比较运算符即可,但引用数据类型不能直接使用比较运算符来比较大小。java给引用数据类型指定了一个标准接口java.lang.Comparable
:
public interface Comparable<T> {
public int compareTo(T o);
}
可通过实现该接口来比较对象的大小:
- 需要比较的类实现接口
java.lang.Comparable
,并重写方法。 - 比较对象大小时,通过对象调用
compareTo
方法,根据方法的返回值决定大小:- this对象(调用
compareTo
方法的对象)减指定对象(传入compareTo
方法的对象)大于0,返回正整数。 - this对象(调用
compareTo
方法的对象)减指定对象(传入compareTo
方法的对象)小于0,返回负整数。 - this对象(调用
compareTo
方法的对象)减指定对象(传入compareTo
方法的对象)等于0,返回0。
- this对象(调用
例:
//Student是包含name(String)、score(int)的标准JavaBean
package org.pojo;
public class Student implements Comparable{
...
@Override
public int compareTo(Object o) {
//o是Object类型,需先向下转型
return this.score - ((Student)o).getScore();
}
}
//测试代码
package org.example;
import org.pojo.Student;
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
//创建数组,存储Student对象
Student[] students = new Student[3];
students[0]=new Student("张三",100);
students[1]=new Student("李四",45);
students[2]=new Student("王五",60);
for(int j=0;j<students.length-1;j++){
for(int i=0;i<students.length-1-j;i++){
//若students[i]比students[i+1]大,则排序换位置
if(students[i].compareTo(students[i+1])>0){
Student temp=students[i];
students[i]=students[i+1];
students[i+1]=temp;
}
}
}
for(int i=0;i<students.length;i++){
System.out.print(students[i].toString()+" ");
}
//Student{name='李四', score=45} Student{name='王五', score=60} Student{name='张三', score=100}
}
}
16.4.2Comparator接口
当有一个类未实现Comparable接口,且不方便对类进行修改,或是已实现该接口,但不想要按照预定义的方法比较大小时,可使用java.util.Comparator
接口。
- 编写类,称之为比较器类型,实现
java.util.Comparator
接口并重写方法。 - 比较大小时,通过比较器类型的对象调用compare()方法,将要比较大小的两个对象作为方法参数传入,根据返回值决定谁大谁小。
//标准JavaBean类Student
package org.pojo;
import java.util.Comparator;
public class Student implements Comparator {
private String name;
private int score;
...
@Override
public int compare(Object o1, Object o2) {
Student s1 = (Student) o1;
Student s2 = (Student) o2;
return s1.getScore() - s2.getScore();
}
}
//测试类
package org.example;
import org.pojo.Student;
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Student student1 = new Student("张三",65);
Student student2 = new Student("李四",76);
System.out.println(student1.compare(student1,student2));
}
}
十七、String类
17.1String介绍与基本原理
Java中所有字符串字面值(如"abc")都作为此类的实例(对象)实现。
- 1.不可变性:字符串是常量,String对象一旦创建,其值不可更改因其底层由
final修饰:
public final class String{...}
而对于常见的字符串加法:
String s="hello";
s+="world";
此时会产生新对象。内存图如下:
- 字符串s本身是引用数据类型的对象,故"hello"在堆中生成。
- 当执行
s+="world";
时,不会对原对象进行修改,而是生成新的对象"helloworld",此时变量s指向该对象,
事实上,String对象是不可变 ,所以可以共享:
- 变量s1、s2保存在栈中,而二者所指向的对象保存在堆中。
- 事实上,变量s2并不会单独在堆中创建对象,而是共享之前已有的"abs"。
package org.example;
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
String s1="abc";
String s2="abc";
System.out.println(s1==s2); //true,引用数据类型比较的是对象的引用,故s1与s2指向的是同一对象
}
}
在jdk8中,String底层是一个被final修饰的char数组,即private final char[] value;
,但在jdk9后,其底层是一个被final修饰的byte数组,即private final byte[] value;
(byte只占1字节,而char占2字节)。正是由于底层是数组,故赋值操作只会更换对象的引用,而不会更改对象本身。
- 2.字符串常量:为了提高性能和节省内存,Java使用了字符串常量池。字符串常量池是一个特殊的内存区域,用于存储字符串常量。当创建一个字符串字面量时(如String str = “Hello”),如果常量池中已经存在相同内容的字符串,就会直接返回常量池中的引用,而不会创建新的对象。
- 3.不可变性的优势:String对象的不可变性使其在多线程环境下是线程安全的,不需要额外的同步措施。并且,由于字符串是不可变的,可以在编译期间进行字符串的共享和缓存,提高程序的性能和效率。
由于String会为字符串常量单独申请一块内存空间,在java代码中描述大量类似的字符串时,可能会造成内存空间的浪费(创建、修改字符串时,实际上原来的字符串对象已经丢弃了。jvm会重新创建一个字符串对象,并令其指向常量池中新的数据空间,容易导致大量副本字符串对象遗留在内存中,降低效率)。为解决上述问题,Java提供了可改变的字符串:
- Stringbuffer:JDK1.0开始存在,属于线程安全,所以效率低下。
- StringBuilder:JDK1.5开始存在,属于非线程安全,所以效率较高。
17.2String的创建
常见创建方式:
package org.example;
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
//1.String()
String s1=new String(); //创建空字符串
//2.String(String original)
String s2=new String("abc");
System.out.println(s2);
//3.String(char[] value)
char[] chars= {'a','b','c'};
String s3=new String(chars);
//4.String(byte[] bytes)
byte[] bytes={97,98,99};
String s4=new String(bytes);//使用平台(操作系统)的默认字符集,即gbk,解码指定的byte数组,构造一个新的String对象
System.out.println(s4); //abc
}
}
事实上,IDEA启动时会自动加一个启动参数-Dfile.encoding=UTF-8
,使得虽然操作系统使用GBK,但IDEA默认使用UTF-8.
package org.example;
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
//1.String(char[] value,int offset,int count)
char[] chars={'a','b','c','d','e','f','g','h'};
String s1=new String(chars,1,3);
System.out.println(s1); //bcd
//2.String(byte[] bytes,int offset,int length)
byte[] bytes={97,98,99,100,101,102,103,104,105};
String s2=new String(bytes,1,3);
System.out.println(s2); //bcd
}
}
17.3内存图分析
例1:
- 创建变量s1时其所指对象"abc"在堆内存当中(实际是常量池,常量池位于堆中)被创建,创建s2时在常量池中找到相同字符串对象,故直接指向,而无需再次创建对象,即此时二者共享对象。
- 创建变量s3时,由于使用new进行创建,故会在堆内存中开辟空间(new一定会创建新空间,该空间不在常量池),此时s3直接指向该空间,但该空间的内容仍共享原"abc"(即会在常量池中寻找是否含有"abc",若有则new并保存对象地址,没有则在常量池中为新对象申请空间,再在s3所new空间中保存该对象在常量池中的地址)。
例2:
String s=new String("abc");
该代码中一共有new和"abc"两个对象,而创建的对象个数,需看"abc"是否提前被创建。
- 若没有提前创建"abc",则会创建两个对象,且先在常量池中创建"abc"再new出空间保存该对象的地址:
- 若提前创建"abc",则会创建一个对象,即在常量池中找到"abc",将地址值赋给new出的空间:
例3:
- 通过反编译可见,s3与s4创建方式相同,而s5、s6使用
new StringBuilder()
进行创建,由于申请了新的空间,故一定引用值一定不同。
总结:字符串拼接时若等号右边是字符串字面值拼接,则不会产生新对象(如s4),但若右侧有变量参与拼接,则会产生产生新的字符串对象。
17.4String常用方法
17.4.1判断字符串相等
boolean equals(String s):比较字符串内容
boolean equalsIgnoreCase(String s):比较字符串内容,忽略大小写
由于字符串类型本身属于引用数据类型,故在比较字符串内容是否相等时应使用equals
方法。当传递来的比较变量本身是null时,若直接使用该对象调用equals
方法会出现空指针的情况,一般的,不会使用字符串变量来调用equals
方法,而使用确定的字符串字面量来调用equals
方法,避免空指针异常。
17.4.2获取字符串属性
package org.example;
public class Main {
public static void main(String[] args) {
String s="abcdefg";
//1.int length():获取字符串长度
System.out.println(s.length());
//2.String concat(String s):字符串拼接,返回新串
System.out.println(s.concat("hijk"));
//3.char charAt(int index):根据索引获取字符
System.out.println(s.charAt(2));
//4.int indexOf(String s):获取指定字符串在原串中第一次出现的索引位置
System.out.println(s.indexOf("cd"));
//5.String subString(int beginIndex,int endIndex):从指定索引截取字符串
System.out.println(s.substring(2,6));
}
}
17.4.3字符串转换方法
package org.example;
import java.io.UnsupportedEncodingException;
public class Main {
public static void main(String[] args) throws UnsupportedEncodingException {
String s="abcdaefga";
//1.char[] toCharArray():将字符串转成char数组
char[] chars=s.toCharArray();
for (int i=0;i<chars.length;i++) System.out.print(chars[i]+" ");
System.out.println();
//2.byte[] getBytes():将字符串转成byte数组
byte[] bytes=s.getBytes();
for (int i=0;i<bytes.length;i++) System.out.print(bytes[i]+" ");
System.out.println();
//3.String replace(CharSequence c1,CharSequence c2):替换字符,其中,CharSequence是String的接口
System.out.println(s.replace("a","c"));
//4.byte[] getBytes(String charsetName):按照指定的编码将字符串转为byte数组
byte[] bytes1="你好".getBytes("utf-8"); //将异常抛出
for(int i=0;i<bytes1.length;i++) System.out.print(bytes1[i]+" ");
System.out.println();
}
}
17.4.4字符串分割方法-略
String[] split(String regex):按照指定的规划分割字符串,regex是正则表达式
17.5StringBuffer
StringBuffer类是Java提供的一个可变字符串序列类,与String不同的是,String类是不可变的,即创建后不能更改其内容,而StringBuffer类则可以在原地修改字符串。具有以下特性:
- 可变性:内容是可变的,可以在原地修改字符串,无需申请空间存储新字符串对象。
- 线程安全:多个线程可以同时访问和修改同一个StringBuffer对象的内容,而不会导致数据不一致。它的方法都被
synchronized
关键字修饰(使用synchronized
修饰实例方法时,它将锁定当前实例对象。只有获得锁的线程才能执行这个方法,其他线程需要等待锁释放后才能访问该方法。),这样可以保证在多线程环境下的线程安全性。 - 高性能:StringBuffer是可变的,避免了创建大量中间字符串的开销,在修改字符串时不需要创建新的字符串对象,而是直接对原始字符串进行修改。
内存图如下(常量池本质在堆中):
public static void main(String[] args) {
StringBuffer sf = new StringBuffer("csdnNB");
}
StringBuffer保存的是 字符串变量,可以直接更改字符串本身的值。因为字符串 变量在 堆内存中,StringBuffer的每次更新实际上可以直接更新字符串的内容,不用每次更新地址,效率较高。只有在某些特殊情况下,比如说该数组预存的空间不足,需要扩容时,才创建新的对象。
17.5.1构造方法
常用构造方法:
方法名 | 功能 |
---|---|
StringBuffer() | 构造一个不带字符的字符串缓冲区,其初始容量为16个字符 |
StringBuffer(int capacity) | 构造一个不带字符,但具有指定初始容量的字符串缓冲区。即可对byte[] value的大小进行指定 |
StringBuffer(String str) | 构造一个字符串缓冲区,并将其内容初始化为指定字符串的内容 |
对于第一个构造器,会依次调用:
//StringBuffer.java
@IntrinsicCandidate
public StringBuffer() {
super(16);
}
//AbstractStringBuilder.java
AbstractStringBuilder(int capacity) {
if (COMPACT_STRINGS) {
value = new byte[capacity];
coder = LATIN1;
} else {
value = StringUTF16.newBytesFor(capacity);
coder = UTF16;
}
}
空参构造时默认初始化大小为16的字节数组:
- capacity参数指定了字符串初始的容量。
- 根据COMPACT_STRINGS的值,决定字符串内部使用byte数组还是char数组来存储字符串。
- 如果COMPACT_STRINGS为true,则使用byte数组存储,编码方式为LATIN1。
- 如果COMPACT_STRINGS为false,则使用char数组存储,编码方式为UTF16。
对于第二个构造器,会依次调用:
//StringBuffer.java
@IntrinsicCandidate
public StringBuffer(int capacity) {
super(capacity);
}
//AbstractStringBuilder.java
AbstractStringBuilder(int capacity) {
if (COMPACT_STRINGS) {
value = new byte[capacity];
coder = LATIN1;
} else {
value = StringUTF16.newBytesFor(capacity);
coder = UTF16;
}
}
- capacity参数指定了字符串初始的容量。
- 根据COMPACT_STRINGS的值,决定字符串内部使用byte数组还是char数组来存储字符串。
- 如果COMPACT_STRINGS为true,则使用byte数组存储,编码方式为LATIN1。
- 如果COMPACT_STRINGS为false,则使用char数组存储,编码方式为UTF16。
对于第三个构造器,会依次调用:
//StringBuffer.java
@IntrinsicCandidate
public StringBuffer(String str) {
super(str);
}
//AbstractStringBuilder.java
AbstractStringBuilder(String str) {
int length = str.length();
int capacity = (length < Integer.MAX_VALUE - 16)
? length + 16 : Integer.MAX_VALUE;
final byte initCoder = str.coder();
coder = initCoder;
value = (initCoder == LATIN1)
? new byte[capacity] : StringUTF16.newBytesFor(capacity);
append(str);
}
- 计算字符串str的长度并将其赋值给length。
- 根据字符串长度计算容量capacity,在长度小于
Integer.MAX_VALUE - 16
时,容量为长度加16,否则为Integer.MAX_VALUE
。 - 获取字符串str的编码格式并赋值给coder。
- 根据编码格式初始化字符存储数组value,如果是LATIN1编码,则创建一个byte数组,否则创建一个UTF-16编码的byte数组。
- 调用append方法将字符串str追加到AbstractStringBuilder对象中。
17.5.2常用方法
方法名 | 功能 |
---|---|
int length() | 获取容器中字符串的有效长度 |
int capacity() | 返回当前容器的容量 |
StringBuffer append(…) | 将字符串添加到容器中 |
StringBuffer delete(int start, int end) | 删除指定序列的部分内容,范围为[start,end) |
StringBuffer replace(int start, int end, String str) | 替换指定序列的部分内容,范围为[start,end) |
StringBuffer reverse() | 反转字符串 |
StringBuffer insert(int offset, String str) | 字符串的指定索引处插入一段字符串 |
package org.example;
import java.io.UnsupportedEncodingException;
public class Main {
public static void main(String[] args) throws UnsupportedEncodingException {
StringBuffer stringBuffer=new StringBuffer("Hello World");
System.out.println("字符串长度为:"+stringBuffer.length());
System.out.println("容器容量为:"+stringBuffer.capacity());
System.out.println("添加内容:"+stringBuffer.append("!"));
System.out.println("删除指定内容"+stringBuffer.delete(0,6));
System.out.println("反转字符串:"+stringBuffer.reverse());
System.out.println("插入一段字符串"+stringBuffer.insert(0,"Hello "));
}
}
17.5.3源码解析
与String相同,StringBuffer使用final关键字修饰,其不可被继承:|
public final class StringBuffer
extends AbstractStringBuilder
implements Appendable, Serializable, Comparable<StringBuffer>, CharSequence{...}
从类图可知,StringBuffer类并没有像String类一样直接继承了Object类,而是直接继承自AbstractStringBuilder类,并且内部使用自动扩容的数组操作字符串,该数组并不在StringBuffer类内部实现,而是继承自AbstractStringBuilder:
abstract sealed class AbstractStringBuilder implements Appendable, CharSequence
permits StringBuilder, StringBuffer {
/**
* The value is used for character storage.
*/
byte[] value;
...
}
与String类不同,该数组无final修饰,因此,StringBuffer字符串实际存放的位置是在堆内存中。这也从根本上解释了为什么StringBuffer是可变字符序列。在构造器源码中:
AbstractStringBuilder(String str) {
int length = str.length();
int capacity = (length < Integer.MAX_VALUE - 16)
? length + 16 : Integer.MAX_VALUE;
final byte initCoder = str.coder();
coder = initCoder;
value = (initCoder == LATIN1)
? new byte[capacity] : StringUTF16.newBytesFor(capacity);
append(str);
}
可见,虽然AbstractStringBuilder类中的byte[] value只是定义了一个字节数组,数组属于引用类型,默认指向为空(即null),但是当通过构造器初始化一个非空的StringBuffer类对象时,很明显在底层有new
操作,故而该数组存储在堆中。即使是空参构造,也会调用父类的一个带参构造:
//StringBuilder.java
@IntrinsicCandidate
public StringBuffer() {
super(16);
}
//AbstractStringBuilder.java
AbstractStringBuilder(int capacity) {
if (COMPACT_STRINGS) {
value = new byte[capacity];
coder = LATIN1;
} else {
value = StringUTF16.newBytesFor(capacity);
coder = UTF16;
}
由此可知,使用空参构造初始化StringBuffer类对象时,底层的byte[]数组默认初始容量为16。
StringBuilder的扩容机制指不断调用append添加字符串时,若底层数组长度不够用则会触发扩容机制,原理如下:
1.append()
//StringBuilder.java
@Override
@IntrinsicCandidate
public StringBuilder append(String str) {
super.append(str);
return this;
}
//AbstractStringBuilder.java
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len);
putStringAt(count, str);
count += len;
return this;
}
- 检查传入的字符串是否为null,如果是则调用appendNull()方法进行处理
- 获取字符串的长度,并通过ensureCapacityInternal方法确保数组可容纳追加后的字符串,传入长度为原字符串长度(而非字符数组长度)+字符串长度。
- 接着,使用putStringAt方法将字符串追加到指定的位置
- 最后,更新容器大小,并返回字符串构建器本身
进入ensureCapacityInternal()方法,此方法用于判断扩容后的数组长度(count + len)是否超出底层数组长度:
//AbstractStringBuilder.java
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
int oldCapacity = value.length >> coder;
if (minimumCapacity - oldCapacity > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity) << coder);
}
}
其中,字符数组最小容量minimumCapacity=更新后字符串长度,函数执行流程为:
- 执行
int oldCapacity = value.length >> coder;
获取当前数组的长度:- 若是拉丁字符(coder=0),则返回字符数组长度。
- 若是Unicode字符(UTF-16,coder=1),则返回的就是value.length/2。
- 比较minimumCapacity(所需的最小容量)和数组长度,若minimumCapacity大于oldCapacity,说明当前数组容量不足,需要扩大。
- 调用newCapacity(minimumCapacity)方法来计算新的容量值,并使用Arrays.copyOf方法创建一个新的容器数组。
- 最后将新创建的数组赋值给value,完成扩容操作。这样,原数组的所有元素会被复制到新数组中,同时新数组具有足够的空间来存储更多的元素。
进入newCapacity(),此方法用于计算数组容量不足时,新数组的容量大小。
//AbstractStringBuilder.java
private int newCapacity(int minCapacity) {
int oldLength = value.length;
int newLength = minCapacity << coder;
int growth = newLength - oldLength;
int length = ArraysSupport.newLength(oldLength, growth, oldLength + (2 << coder));
if (length == Integer.MAX_VALUE) {
throw new OutOfMemoryError("Required length exceeds implementation limit");
}
return length >> coder;
}
- 使用oldLength存储当前数组长度(即当前容量),newLength存储minCapacity乘2后的值作为新数组基础长度(基础新容量),growth存储新字符串与旧数组容量的差值。
- 调用ArraysSupport.newLength方法,传入旧容量、容量增长值和旧容量加上2的coder次幂,计算并返回新的容量值length。
- 若新容量值length等于Integer.MAX_VALUE,则抛出OutOfMemoryError异常,表示所需的容量超出了实现的限制。
- 最后,将新容量值length右移coder位,并返回(在ensureCapacityInternal()中会将其左移coder位)。
扩容结果为:当append的字符长度超过原容量大小时,扩容为原来字节数组的长度*2+2,如若还不够则直接扩容到新字符串长度。等价代码如下(旧版jdk写法):
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
断点处执行后,length等于36(34不够)。
17.6StringBuilder
同StringBuffer一样,StringBuilder类也是一个可变的字符序列,二者拥有相似的API, 但StringBuilder类方法并不适用synchronized
修饰,使得无法保证线程安全。在实际开发中,由于StringBuilder类效率比StringBuffer类还要高,因此,建议在满足单线程的基础上,优先使用StringBuilder类。
17.6.1常用构造方法
StringBuilder构造方法底层与StringBuffer相同,即使用父类AbstractStringBuilder功能来创建字符串容器。
17.6.2常用成员方法
与StringBuffer不同,StringBuilder大多数方法都使用synchronized
,故而保证了线程安全。
17.6.3源码解析
public final class StringBuilder
extends AbstractStringBuilder
implements Appendable, java.io.Serializable, Comparable<StringBuilder>, CharSequence
{...}
StringBuilder同样被final关键字修饰而无法被继承,并且继承了AbstractStringBuilder类,即使用父类中的字节数组保存字符串。
十八、包装类
Java中将8种基本数据类型进行了封装,包装类是基本类型对应的类,将基本类型转换为对应的包装类后,使其拥有了类的特性,可以使用包装类中的方法操作数据。
其中,除了Boolean和Character这两个包装类外,其他六大包装类都继承自Number类,并且它们都实现了一些接口。
在JavaBean中推荐使用包装类型,如定义用户序号属性uid时,由于JavaBean中的数据都是和数据库表相联系,若设置uid为主键自增,则此时添加语句中的uid就无需单独维护,SQL语句为:
insert into user(uid,username,password)values(NULL,'张三','123456')
其中,uid为包装类型,默认值为NULL。
18.1装箱与拆箱
- 拆箱:包装类型——>基本类型
- 装箱:基本类型——>包装类型
package java.lang;
public abstract class Number implements java.io.Serializable {
public Number() {super();}
public abstract int intValue();
public abstract long longValue();
public abstract float floatValue();
public abstract double doubleValue();
public byte byteValue() {
return (byte)intValue();
}
public short shortValue() {
return (short)intValue();
}
@java.io.Serial
private static final long serialVersionUID = -8742448824652078965L;
}
手动装/拆箱
父类(抽象类)Number中提供了手动拆箱函数XXValue()
(不包括boolean和char)将包装类型转换为基本类型(或用于转换为其他基本类型),而所有包装类中都提供了valueOf()
方法完成手动装箱或是将字符串转换为对应的包装类:
int i=5;
//1.手动装箱
Integer integer = Integer.valueOf(i);
//2.手动拆箱
int j = integer.intValue();
对于包装类Boolean和Charracter,二者内部实现了XXValue()
方法用于手动拆箱,但不含除自身之外的XXValue()
方法,因为二者数据较为特殊,和其他基本数据类型没有关联。
自动装/拆箱
Java提供了自动装/拆箱机制以减少代码量,
int i=5;
//1.自动装箱
Integer i1=i;
Integer i2=(Integer) i;
//2.自动拆箱
Integer j=i1;
int j1=j;
float j2=(float)j;
查看字节码文件:
public static void main(String[] args) {
int i = 5;
Integer i1 = Integer.valueOf(i);
Integer i2 = Integer.valueOf(i);
Integer j = i1;
int j1 = j;
float j2 = (float)j;
}
即,自动装箱操作在底层实际是调用了valueOf方法,将基本数据类型转为包装类;自动拆箱操作也是在底层调用了intValue方法(字节码文件未显示出),将包装类转为基本数据类型。并且,在自动装箱时会进行类型是否匹配的检测,包装类类型和基本数据类型之间进行赋值时必须得类型对应,否则编译出错。而在自动拆箱时,类型匹配会适当放宽,自动拆箱时会转换为包装类对应的基本数据类型,此时赋值给某个变量,这个变量不需要类型完全匹配,精度大于等于赋值的数据都可以。事实上,编译器会进行隐式类型转换,如果精度不匹配则需要显示强转,即获得对应类型的基本数据类型后再进行强转。注意,包装类类型是不能直接强转为基本数据类型的,基本数据类型也不能直接强转为包装类类型。
18.2缓存池原理
Java 5中引入了Integer缓存池以节省内存、提高性能,事实上,Integer中有静态内部类IntegerCache
,其成员cache[]
即为Integer常量池,大小为1B,用于存储-128~127
的数据。
private static final class IntegerCache {
static final int low = -128;
static final int high;
@Stable
static final Integer[] cache;
static Integer[] archivedCache;
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
CDS.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
当创建Integer对象时,若不使用new语句创建(与字符串常量池原理类似),且值在-128~127
之间,则会直接返回常量值中数据的地址,即实现数据共享。而若超出了范围,会从堆区new一个Integer对象来存放值。Integer.valueOf()
代码为:
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
JVM初始化过程中,java.lang.Integer.IntegerCache.high属性可能会被设置并保存在sun.misc.VM类的私有系统属性中,使得该缓存在第一次使用时即可完成初始化,且缓存大小可由-XX:AutoBoxCacheMax=<size>
选项控制。
&emsp其他包装类的缓存池情况如下:
- Boolean:全部缓存。
- Byte:全部缓存。
- Character:0~127。
- Short:-128~127。
- Long:-128~127。
- Float:没有缓存。
- Double:没有缓存。
在缓存值机制的作用下,以Integer为例,在-128~127的Integer值且使用Integer x = value;
的方式赋值的Integer值在进行和equals比较时,都会返回true。但若使用Integer x = new Integer("127");
方式创建,还是和以前创建对象引用一样,在堆区中创建一个对象,然后将栈中引用指向这个对象,使用比较时,数据相同,但仍返回false。
&emsp可手动修改配置:
-XX:AutoBoxCacheMax=128
package org.example;
public class Main {
public static void main(String[] args) {
Integer i=128;
Integer j=128;
System.out.println(i==j); //true
}
}
注意,从IntegerCache类中代码可知,虽然修改-XX:AutoBoxCacheMax
可以修改缓存池的最大范围,但需要大于127才能生效,小于等于127时,依然取的是默认值127
18.3String类的转化
18.3.1String转为基本类型
在除Character外的包装类都提供了parseXxx
方法将字符串类型的数据转换为对应的基本类型
package org.example;
public class Main {
public static void main(String[] args) {
//parseXxx(String),以对应的基本类型作接收
byte temp_byte = Byte.parseByte("11");
short temp_short = Short.parseShort("141");
int temp_int = Integer.parseInt("430");
long temp_long = Long.parseLong("11211");
float temp_float = Float.parseFloat("66.66F");
double temp_double = Double.parseDouble("666.666");
boolean temp_boolean = Boolean.parseBoolean("true");
System.out.println("temp_byte = " + temp_byte);
System.out.println("temp_short = " + temp_short);
System.out.println("temp_int = " + temp_int);
System.out.println("temp_long = " + temp_long);
System.out.println("temp_float = " + temp_float);
System.out.println("temp_double = " + temp_double);
System.out.println("temp_boolean = " + temp_boolean);
}
}
而若想将字符串类型转为char类型,可通过String类中的toCharArray()
方法和charAt()
方法实现:
package org.example;
public class Main {
public static void main(String[] args) {
//1.使用toCharArray()方法将字符串转为字符数组
String string="Hello World";
char[] chars=string.toCharArray();
for(int i=0;i<chars.length;i++) System.out.printf("chars[i]=="+chars[i]+" ");
System.out.println();
//2.使用charAt()直接获取字符串中的字符
System.out.println("string.charAt(0)=="+string.charAt(0));
}
}
18.3.2基本类型转为String
基本类型转为String有两种方式:
- 直接与空字符串拼接。
- 使用String类的valueOf()方法
package org.example;
public class Main {
public static void main(String[] args) {
//方法一 : 以空字符串拼接的形式
//byte --> String
byte temp_byte = 127;
String temp_string_0 = 127 + "";
//short --> String
short temp_short = 141;
String temp_string_1 = temp_short + "";
//int --> String
int temp_int = 428;
String temp_string_2 = temp_int + "";
//long --> String
long temp_long = 11211;
String temp_string_3 = temp_long + "";
//float --> String
float temp_float = 135.0F;
String temp_string_4 = temp_float + "";
//double --> String
double temp_double = 433.0;
String temp_string_5 = temp_double + "";
//char --> String
char temp_char = 'A';
String temp_string_6 = temp_char + "";
//boolean --> String
boolean temp_boolean = true;
String temp_string_7 = temp_boolean + "";
System.out.println("temp_string_0 = " + temp_string_0);
System.out.println("temp_string_1 = " + temp_string_1);
System.out.println("temp_string_2 = " + temp_string_2);
System.out.println("temp_string_3 = " + temp_string_3);
System.out.println("temp_string_4 = " + temp_string_4);
System.out.println("temp_string_5 = " + temp_string_5);
System.out.println("temp_string_6 = " + temp_string_6);
System.out.println("temp_string_7 = " + temp_string_7);
System.out.println("========================================");
//方法二 : 利用String类的valueOf方法
temp_string_0 = String.valueOf(temp_byte) + "_EX";
temp_string_1 = String.valueOf(temp_short) + "_EX";
temp_string_2 = String.valueOf(temp_int) + "_EX";
temp_string_3 = String.valueOf(temp_long) + "_EX";
temp_string_4 = String.valueOf(temp_float) + "_EX";
temp_string_5 = String.valueOf(temp_double) + "_EX";
temp_string_6 = String.valueOf(temp_char) + "_EX";
temp_string_7 = String.valueOf(temp_boolean) + "_EX";
System.out.println("temp_string_0 = " + temp_string_0);
System.out.println("temp_string_1 = " + temp_string_1);
System.out.println("temp_string_2 = " + temp_string_2);
System.out.println("temp_string_3 = " + temp_string_3);
System.out.println("temp_string_4 = " + temp_string_4);
System.out.println("temp_string_5 = " + temp_string_5);
System.out.println("temp_string_6 = " + temp_string_6);
System.out.println("temp_string_7 = " + temp_string_7);
}
}
18.3.3String转包装类
除Character的包装类外都提供了parseXxx()
方法将String类型转为基本类型,此时可通过包装类型变量接收实现自动装箱(valueOf方法),而Character则可使用charAt()方法实现。
package org.example;
import static java.lang.Double.parseDouble;
import static java.lang.Float.parseFloat;
import static java.lang.Integer.parseInt;
import static java.lang.Long.parseLong;
import static java.lang.Short.parseShort;
public class Main {
public static void main(String[] args) {
Integer i1=parseInt("123");
Short i2=parseShort("32");
Long i3=parseLong("123");
Float i4=parseFloat("123");
Double i5=parseDouble("12.3");
Byte i6= Byte.parseByte("32");
Character i7="Hello World".charAt(3);
}
}
18.3.4包装类转String
包装类转String有三种方式:
String xxx=包装类变量名+"";
String xxx=包装类类名.toString(包装类变量名);
String xxx=String.valueOf(包装类变量名);
例:
package org.example;
public class Main {
public static void main(String[] args) {
//方式一 :
Integer integer_0 = 141; //自动装箱
String string_0 = integer_0 + "";
//方式二 :
Integer integer_1 = 135;
String string_1 = Integer.toString(integer_1) + " hello";
//方式三 :
Integer integer_2 = 431;
String string_2 = String.valueOf(integer_2) + " world";
}
}
十九、多线程
19.1基础知识
- 进程:在内存中执行的应用程序。
- 线程:进程中最小的执行单元,负责当前进程中程序的执行。一个进程中至少有一个线程,拥有多个线程的进程(应用程序)称为多线程。
- 并行:同一时刻,有多个线程在多个CPU(普遍是多核CPU)上同时执行。
- 并发:一段时间内,有多个指令在单个CPU上交替执行。
在早期的单核CPU中,多道程序同时执行的本质是CPU在多个线程之间高速切换。目前CPU普遍是多核结构,如2核4线程CPU,指CPU可同时运行4个线程且无需切换,但当超出四个线程时,CPU仍需在多个线程之间进行高速切换,这就涉及到调度算法。常见CPU调度算法有:
- 分时调度:所有线程轮流获取CPU使用权,并且平均分配每个线程占用CPU的时间片。
- 抢占式调度:多个线程轮流抢占CPU使用权,可根据线程优先级进行调度。Java程序采用的是抢占式调度
其中,主线程是CPU和内存之间开辟的专门为main方法服务的线程:
19.2线程的创建
在API文档中提供了两种线程创建方式:
- 继承Thread类,重写Thread类的run()方法后即可分配并启动子类的实例。
//定义线程类
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
//重写run()方法
public void run() {
// compute primes larger than minPrime
. . .
}
}
//main函数中创建线程并开始执行
PrimeThread p = new PrimeThread(143);
p.start(); //开始执行线程,JVM调用此线程的run()方法
- 实现Runnable接口,并实现run()方法,之后可创建该类的实例,在创建线程时将该实例作为参数传递并启动
//定义线程类
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
//重写run()方法
public void run() {
// compute primes larger than minPrime
. . .
}
}
//main函数中创建线程并开始执行
PrimeRun p = new PrimeRun(143);
new Thread(p).start(); //开始执行线程,JVM调用此线程的run()方法
19.2.1Thread创建方式
创建流程
- 定义自定义线程类,继承Thread。
- 重写run方法,在run方法中设置线程任务,即线程的执行代码。
- 创建自定义线程类对象,调用Thread中的start方法开启线程,此时jvm会自动调用run方法开始执行。注意,不能直接调用run方法,否则仅仅只是执行该方法,而不是创建一个新线程。
//MyThread.java
package org.SelfDefinedThread;
public class MyThread extends Thread{
public void run() {
for(int i=0;i<10;i++){
System.out.println("Thread:"+i);
}
}
}
//Main.java
package org.example;
import org.SelfDefinedThread.MyThread;
public class Main {
public static void main(String[] args) {
//1.创建线程对象
MyThread t1 = new MyThread();
//2.执行start方法,jvm自动执行run方法
t1.start();
//3.在主线程中执行相同输出操作
for(int i=0;i<10;i++) System.out.println("main:"+i);
}
}
两次执行结果不一致说明二者的执行仍具有不可再现性。
内存分析
- 当执行
t1.start()
开启新线程后,内存当中会为该线程开辟新的栈空间,负责执行新线程中的方法。 - 注意,同一个线程对象不能连续调用多次start方法,只能再次创建新线程对象并开启。
19.2.2Runnable创建方式
Thread当中的run方法即是实现了Runnable接口当中的方法(也是Runnable的唯一方法),实现Runnable接口创建线程的方式为:
- 创建自定义线程类,实现Runnable接口。
- 重写run方法,设置线程任务。
- 调用Thread类的构造方法
Thread(Runnable target)
,将自定义的类作为参数传入以创建线程。 - 调用Thread中的start方法让JVM调用run方法开启线程。
//Main.java
package org.example;
import org.SelfDefinedThread.MyRunnable;
public class Main {
public static void main(String[] args) throws InterruptedException {
MyRunnable myRunnable=new MyRunnable();
Thread thread=new Thread(myRunnable);
thread.start();
}
}
//MyRunnable.java
package org.SelfDefinedThread;
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("Runnable线程执行");
}
}
Thread创建方式与Runnable创建方式的区别
- Thread创建方式:继承只支持单继承,有继承的局限性,即自定义线程类无法继承其他类。
- Runnable创建方式:没有继承的局限性,自定义线程类可继承父类的同时实现Runnable接口。
19.2.3匿名内部类创建方式
匿名内部类创建方式严格来说并不是一种新的创建方式。
1.创建Thread对象
public class Main {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("使用匿名内部类创建 Thread 子类对象");
}
};
thread.start();
}
}
2.实现Runnable接口
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名内部类,实例Runnable接口作为构造参数");
}
});
thread.start();
}
}
19.2.4Callable接口
Callable<T>
接口类似于Runnable,接口中方法为:
//Callable.java
@FunctionalInterface
public interface Callable<V> {
//计算结果,若无法计算则抛出异常
V call() throws Exception;
}
//Runable.java
@FunctionalInterface
public interface Runnable {
void run();
}
call()
类似于run方法,都是用于设置线程任务,不同点在于:
- call()有返回值。
- call()能抛出异常,而实现了Runnable接口的类由于run没有抛出异常故也无法抛出异常。
对于<T>
,其表示泛型,用于指定需要操作的数据(在继承时就应指明),且T只能是引用数据类型,若不指明,则默认是Object。实现Callable接口时,指定泛型是什么类型,返回值就是什么类型。例:
由于call()方法的返回值是泛型,故Java提供了FutureTask<V>
类进行接收。在FutureTask<V>
有方法get(),用于获取call方法的返回值:
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
/**
* @throws CancellationException {@inheritDoc}
*/
public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
return report(s);
}
使用Callable
接口的方式:
//MyCallable.java
package org.SelfDefinedThread;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "MyCallable:call";
}
}
//Main.java
package org.example;
import org.SelfDefinedThread.BaoZiPu;
import org.SelfDefinedThread.MyCallable;
import org.SelfDefinedThread.Product;
import org.SelfDefinedThread.Customer;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyCallable myCallable = new MyCallable();
/*
* FutureTask(Callable<V> callable)
* */
FutureTask<String> futureTask=new FutureTask<>(myCallable);
//创建Thread对象(FutureTask实现了泛型接口RunnableFuture,而RunnableFuture实现了接口Runnable,即FutureTask也是Runnable的实现类)
Thread thread=new Thread(futureTask);
thread.start();
//抛出两个异常
System.out.println(futureTask.get());
}
}
19.2.5线程池
在上述代码中,没执行一个线程任务,就需要创建一个线程对象去执行,用完后还要销毁线程对象,频繁的创建、销毁线程会增大内存资源的消耗,而线程池是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。事实上,这些任务就是实现了Runnable或Callable接口的实例对象。线程池的优势在于:
- 线程和任务分离,提升线程重用性。
- 控制线程并发数量,降低服务器压力,统一管理所有线程。
- 提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间。
线程池使用流程:
- 使用线程池类
Executors
。 - 使用
Executors
类中的静态方法static ExecutorService newFixedThreadPool(int nThreads)
创建线程池对象。int nThreads
用于指明线程池容量。- 返回值
ExecutorService
即为获取的线程池对象。
- 使用
ExecutorService
的submit()
方法执行线程任务,其中,submit()
的返回值需使用Future接口进行接收(run()无返回值无需接收,而call()、submit()均需使用Future接口进行接收)。Future<?> submit(Runnable task)
:提交一个Runnable任务。<T> Future<T> submit(Callable<T> task)
:提交一个Callable任务。
- 使用Future下的get()方法获取返回值。
- 使用
Executors
的void shutdown()
方法关闭线程池,且线程池中的线程有序关闭,即先前提交的任务仍将执行,但不会接收任何新任务。
例1:使用Runnable
//MyRunnable.java
package org.SelfDefinedThread;
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":run()");
}
}
//Main.java
package org.example;
import org.SelfDefinedThread.MyRunnable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(2);
//执行线程任务(没有返回值就无需接收)
es.submit(new MyRunnable());
es.submit(new MyRunnable());
es.submit(new MyRunnable());
//关闭线程池对象
es.shutdown();
}
}
注意,若无es.shutdown();
,则程序不会自动停止运行,此时线程池仍在等待线程任务。
例2:使用Callable
package org.example;
import org.SelfDefinedThread.MyCallable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(2);
//执行线程任务(存在返回值就需要接收)
FutureTask<String> ft1 = (FutureTask<String>) es.submit(new MyCallable());
System.out.println(ft1.get());
FutureTask<String>ft2= (FutureTask<String>) es.submit(new MyCallable());
System.out.println(ft2.get());
FutureTask<String>ft3= (FutureTask<String>) es.submit(new MyCallable());
System.out.println(ft3.get());
//关闭线程池对象
es.shutdown();
}
}
19.3Thread常用方法
在Thread类中提供了以下常用方法:
方法名 | 功能 |
---|---|
void run() | 实现线程功能 |
void start() | 开启线程 |
String getName() | 获取线程的名称 |
void setName() | 修改线程的名称 |
static Thread currentThread() | 返回当前执行线程对象的引用 |
static void sleep(long millis) | 线程睡眠,超时后线程继续执行,参数为毫秒值 |
void setPriority(int newPriority) | 更改线程优先级。优先级越高的线程 |
int getPriority() | 获取线程优先级 |
void setDaemon(boolean on) | 设置为守护线程 |
static void yield() | 礼让线程,即让当前线程让出CPU使用权 |
void join() | 插入线程 |
run()、start()、getName()、setName()、currentThread()
//Main.java
package org.example;
import org.SelfDefinedThread.MyThread;
public class Main {
public static void main(String[] args) {
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//开启线程
t1.start();
t2.start();
//getName():获取当前执行线程的名称
System.out.println("t1:"+t1.getName()); //ThreadName:Thread-0
System.out.println("t2:"+t2.getName()); //ThreadName:Thread-1
//setName():设置线程名称
t1.setName("Thread-A");
t2.setName("Thread-B");
//currentThread():获取当前执行线程对象
System.out.println("MainThreadName:"+Thread.currentThread().getName()); //MainThreadName:main
}
}
//MyThread.java
package org.SelfDefinedThread;
public class MyThread extends Thread{
public void run() {
}
}
注意,由于MyThread是Thread的子类,故在MyThread内部可直接调用以上方法而无需通过对象调用。
sleep()
//Main.java
package org.example;
import org.SelfDefinedThread.MyThread;
public class Main {
public static void main(String[] args) {
//创建线程对象
MyThread myThread=new MyThread();
//开启线程
myThread.start();
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
//MyThread.java
package org.SelfDefinedThread;
public class MyThread extends Thread{
public void run() {
for(int i=0;i<3;i++){
//线程睡眠1s
try {
Thread.sleep(1000); //sleep()有异常抛出
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName()+i);
}
}
}
其中,先输出前三行,后三行间隔1秒依次输出。注意,Thread类的run方法中并未在函数声明抛出异常,故重写的run方法也不能。
setPriority()、getPriority()
setPriority()
:用于设置线程优先级,优先级越高的线程抢到CPU使用权的几率越大,但并非每次都先抢到。getPriority()
:获取线程优先级。
在创建线程时会为线程分配默认优先级:
public class Thread implements Runnable {
...
//最低优先级
public static final int MIN_PRIORITY = 1;
//默认优先级
public static final int NORM_PRIORITY = 5;
//最高优先级
public static final int MAX_PRIORITY = 10;
...
}
效果并不明显:
//Main.java
package org.example;
import org.SelfDefinedThread.MyThread;
public class Main {
public static void main(String[] args) {
//创建线程对象
MyThread myThread0=new MyThread();
MyThread myThread1=new MyThread();
//获取线程默认优先级
System.out.println("myThread1 Priority:"+myThread0.getPriority()); //myThread1 Priority:5
System.out.println("myThread2 Priority:"+myThread1.getPriority()); //myThread2 Priority:5
System.out.println("main Priority:"+Thread.currentThread().getPriority()); //main Priority:5
//设置进程优先级
myThread0.setPriority(Thread.MIN_PRIORITY);
myThread1.setPriority(Thread.MAX_PRIORITY);
//开启线程
myThread0.start();
myThread1.start();
}
}
//MyThread.java
package org.SelfDefinedThread;
public class MyThread extends Thread{
public void run() {
for(int i=0;i<3;i++){
System.out.println(getName()+":"+i);
}
}
}
setDaemon()
setDaemon(boolean on)
:设置守护线程,当非守护线程执行完毕,守护线程就要结束(守护线程并非立刻结束,在收到系统传递非守护线程结束执行的信息后,守护线程才能结束执行)。
//Main.java
package org.example;
import org.SelfDefinedThread.MyThread;
import org.SelfDefinedThread.MyThread1;
public class Main {
public static void main(String[] args) {
//创建线程对象
MyThread myThread0=new MyThread();
MyThread1 myThread1=new MyThread1();
//设置守护线程
myThread1.setDaemon(true);
//开启线程
myThread0.start();
myThread1.start();
}
}
//MyThread.java
package org.SelfDefinedThread;
public class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(getName()+":"+i);
}
}
}
//MyThread1.java
package org.SelfDefinedThread;
public class MyThread1 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(getName()+":"+i);
}
}
}
yield()
static void yield()
:礼让线程,即让当前线程让出CPU使用权。
若两个线程同时执行,可能出现A线程执行完后再执行B线程,为使得A与B线程均衡地交替执行,可设置礼让线程(只能相对礼让,如线程A礼让线程B,但仍可能抢到CPU执行权)。
package org.SelfDefinedThread;
public class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(getName()+":"+i);
Thread.yield();
}
}
}
void join()
void join()
:插入线程。
插入线程可用于调整线程的执行顺序。
package org.example;
import org.SelfDefinedThread.MyThread;
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread myThread0=new MyThread();
myThread0.start();
myThread0.join(); //将myThread0插入到当前线程(main)之前
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
19.4线程安全
当多个线程访问同一资源时,就可能出现线程安全问题。例如,三个用户通过app、窗口、官网三种渠道买票,可能出现由于CPU支持多线程执行,使得票1同时被多个用户选取,出现线程安全问题。以代码为例:
//MyTicket.java
package org.SelfDefinedThread;
public class MyTicket implements Runnable{
//定义100张票
int ticket = 100;
@Override
public void run() {
while(true){
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买到票:" + ticket);
ticket--;
}
}
}
}
//Main.java
package org.example;
import org.SelfDefinedThread.MyTicket;
public class Main {
public static void main(String[] args) throws InterruptedException {
MyTicket myTicket1 = new MyTicket();
//创建三个买票线程
Thread thread1=new Thread(myTicket1,"张三");
Thread thread2=new Thread(myTicket1,"李四");
Thread thread3=new Thread(myTicket1,"王五");
//开始抢票
thread1.start();
thread2.start();
thread3.start();
}
}
可见,由于买票并非原子操作(一个线程在run函数内部执行完输出语句还未执行自减操作时,CPU切换到另一线程执行,此时新线程会对同一张票重复操作),使得存在一张票被多人买到的情况 。
为此,有同步代码块与同步方法两种方式保证线程的安全执行。
19.4.1同步代码块:synchronized关键字
使用格式:
synchronized(任意对象){
线程可能出现不安全的代码;
}
其中,任意对象
指的就是锁对象。一个线程拿到锁后,会进入同步代码块中执行,在此期间,其他线程无法进入代码块执行(只能在代码块外等待),从而解决线程不安全问题。而当进入代码块的线程退出时,相当于释放锁,抢到该锁的线程就能够进入代码块并执行。
//Main.java
package org.example;
import org.SelfDefinedThread.MyTicket;
public class Main {
public static void main(String[] args) throws InterruptedException {
MyTicket myTicket1 = new MyTicket();
//创建三个买票线程
Thread thread1=new Thread(myTicket1,"张三");
Thread thread2=new Thread(myTicket1,"李四");
Thread thread3=new Thread(myTicket1,"王五");
//开始抢票
thread1.start();
thread2.start();
thread3.start();
}
}
//MyTicket.java
package org.SelfDefinedThread;
public class MyTicket implements Runnable{
//定义100张票
int ticket = 100;
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (this){ //此处使用myTicket1对象本身作为三个线程的锁
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买到票:" + ticket);
ticket--;
}
}
}
}
}
此处使用了sleep()
避免一个线程可同时买过多的票。
注意,以上三个线程使用的是同一把锁(三个线程使用同一个MyTicket对象创建,即三个线程调用run方法本质都是由myTicket1进行调用),而当不使用同一把锁时,就无此制约作用。
19.4.2同步方法
同步方法使用关键字synchronized
修饰,同一时间只能有一个线程访问该方法。这种机制主要用于解决多线程环境下的数据一致性问题,确保对共享资源的访问是安全的。同步方法的使用可以防止多个线程同时访问同一资源,从而避免数据的不一致性。
1.普通同步方法
修饰符 synchronized 返回值类型 方法名(参数){
线程不安全代码;
return 结果;
}
//默认锁:this
例:
package org.SelfDefinedThread;
public class MyTicket implements Runnable{
//定义100张票
int ticket = 100;
//任意创建一个对象
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
method();
}
}
public synchronized void method(){
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买到票:" + ticket);
ticket--;
}
}
}
这种非静态的同步方法默认锁是this
(即myTicket1),等价于:
public synchronized void method(){
synchronized (this){
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买到票:" + ticket);
ticket--;
}
}
}
2.静态同步方法
修饰符 static synchronized 返回值类型 方法名(参数){
线程不安全代码;
return 结果;
}
//默认锁:class对象
例:
package org.SelfDefinedThread;
public class MyTicket implements Runnable{
//定义100张票
static int ticket = 100; //需修改为静态成员变量
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
method();
}
}
public static synchronized void method(){
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买到票:" + ticket);
ticket--;
}
}
}
由于静态同步方法默认锁是class对象,故上树代码等价于:
public static void method(){
synchronized (MyTicket.class){
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买到票:" + ticket);
ticket--;
}
}
}
19.5死锁
死锁指的是两个或两个以上的线程在执行的过程中由于竞争同步锁而产生的一种阻塞现象,若没有外力的作用,它们将无法继续执行下去,这种情况称只为死锁。
在上图中,线程1正在持有锁1,但是线程1必须再拿到锁2才能继续执行。而线程2正在持有锁2,需要再拿到锁1才能继续执行。此时两个线程都处于互相等待的状态,这就是死锁,死锁往往出现在同步代码块的嵌套当中。
例:
//设置锁1:LockA.java
package org.SelfDefinedThread;
public class LockA {
public static LockA lockA=new LockA();
}
//设置锁2:LockB.java
package org.SelfDefinedThread;
public class LockB {
public static LockB lockB = new LockB();
}
//MyRunnable.java
package org.SelfDefinedThread;
public class MyRunnable implements Runnable{
private boolean flag;
public MyRunnable(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag==true){ // flag==true的线程,先获取lockA锁,再获取lockB锁
synchronized (LockA.lockA) {
System.out.println("lockA1");
synchronized (LockB.lockB) {
System.out.println("lockB1");
}
}
}else { // flag==false的线程,先获取lockB锁,再获取lockA锁
synchronized (LockB.lockB) {
System.out.println("lockB2");
synchronized (LockA.lockA) {
System.out.println("lockA2");
}
}
}
}
}
//Main.java
package org.example;
import org.SelfDefinedThread.MyRunnable;
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable1=new MyRunnable(true);
MyRunnable myRunnable2=new MyRunnable(false);
new Thread(myRunnable1).start();
new Thread(myRunnable2).start();
}
}
19.6线程状态
线程被创建并启动后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,Thread类中提供了state
枚举成员来描述线程的六种状态,含义如下:
19.7等待唤醒机制
19.7.1等待唤醒案例分析
一个线程生产(生产者),一个线程消费(消费者),且不能连续生产、消费,二者通过等待唤醒机制进行通信。
方法 | 说明 |
---|---|
void wait() | 线程等待,等待过程中线程释放锁,需被其他线程调用notify方法将其唤醒,重新枪锁执行。 |
void notify() | 线程唤醒,一次唤醒一个等待线程,若有多个线程等待,则随机唤醒一条等待线程。 |
void notifyAll() | 唤醒所有等待线程 |
注意:由于wait()与notify()方法需锁对象调用,所以需用到同步代码块中,且必须是同一锁对象。
19.7.2等待唤醒机制实现
//创建自定义锁类:BaoZiPu.java
package org.SelfDefinedThread;
public class BaoZiPu {
//代表包子的count
private int count;
//代表是否有包子的flag
private boolean flag;
public BaoZiPu() {
}
public BaoZiPu(int count, boolean flag) {
this.count = count;
this.flag = flag;
}
//getCount()表示消费包子
public void getCount() {
System.out.println("消费第"+count+"个包子");
}
//setCount()表示生产包子
public void setCount() {
count++;
System.out.println("生产第"+count+"个包子");
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
//创建生产者类:Product.java
package org.SelfDefinedThread;
public class Product implements Runnable{
private BaoZiPu baoZiPu;
public Product(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(500); //降低循环执行速度
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (baoZiPu){
if(baoZiPu.isFlag()){ //若包子铺有包子,则等待
try {
baoZiPu.wait(); //释放锁(阻塞当前线程)
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{ //若包子铺没有包子,则生产包子
baoZiPu.setCount();
baoZiPu.setFlag(true); //此处是生产一个消费一个,不存在连续生产/消费
baoZiPu.notify(); //唤醒消费线程(一共只有消费/生产两个线程,且二者不能同时执行)
}
}
}
}
}
//创建消费者类:Customer.java
package org.SelfDefinedThread;
public class Customer implements Runnable{
private BaoZiPu baoZiPu;
public Customer(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(500); //降低循环执行速度
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (baoZiPu){
if(!baoZiPu.isFlag()){ //若包子铺没有包子,则等待
try {
baoZiPu.wait(); //释放锁(阻塞当前线程)
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{ //若包子铺有包子,则消费包子
baoZiPu.getCount();
baoZiPu.setFlag(false); //此处是生产一个消费一个,不存在连续生产/消费
baoZiPu.notify(); //唤醒消费线程(一共只有消费/生产两个线程,且二者不能同时执行)
}
}
}
}
}
//Main.java
package org.example;
import org.SelfDefinedThread.BaoZiPu;
import org.SelfDefinedThread.Product;
import org.SelfDefinedThread.Customer;
public class Main {
public static void main(String[] args) {
//创建锁对象
BaoZiPu baoZiPu=new BaoZiPu(0,false);
Product product=new Product(baoZiPu);
Customer customer=new Customer(baoZiPu);
Thread threadProduct=new Thread(product);
Thread threadCustomer=new Thread(customer);
threadProduct.start();
threadCustomer.start();
}
}
19.7.3同步方法实现等待唤醒
将上述案例中线程生产/消费包子的操作使用同步方法进行封装:
//BaoZiPu.java
package org.SelfDefinedThread;
public class BaoZiPu {
//代表包子的count
private int count;
//代表是否有包子的flag
private boolean flag;
public BaoZiPu() {
}
public BaoZiPu(int count, boolean flag) {
this.count = count;
this.flag = flag;
}
//getCount()表示消费包子
public synchronized void getCount() {
if(!this.isFlag()){ //若包子铺没有包子,则等待
try {
this.wait(); //释放锁(阻塞当前线程)
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("消费第"+count+"个包子");
this.setFlag(false); //此处是生产一个消费一个,不存在连续生产/消费
this.notify(); //唤醒消费线程(一共只有消费/生产两个线程,且二者不能同时执行)
}
//setCount()表示生产包子
public synchronized void setCount() {
if(this.isFlag()){ //若包子铺有包子,则等待
try {
this.wait(); //释放锁(阻塞当前线程)
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
count++;
System.out.println("生产第"+count+"个包子");
this.setFlag(true); //此处是生产一个消费一个,不存在连续生产/消费
this.notify(); //唤醒消费线程(一共只有消费/生产两个线程,且二者不能同时执行)
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
//Product.java
package org.SelfDefinedThread;
public class Product implements Runnable{
private BaoZiPu baoZiPu;
public Product(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(500); //降低循环执行速度
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
baoZiPu.setCount();
}
}
}
//Customer.java
package org.SelfDefinedThread;
public class Customer implements Runnable{
private BaoZiPu baoZiPu;
public Customer(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(500); //降低循环执行速度
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
baoZiPu.getCount();
}
}
}
//Main.java
package org.example;
import org.SelfDefinedThread.BaoZiPu;
import org.SelfDefinedThread.Product;
import org.SelfDefinedThread.Customer;
public class Main {
public static void main(String[] args) {
//创建锁对象
BaoZiPu baoZiPu=new BaoZiPu(0,false);
Product product=new Product(baoZiPu);
Customer customer=new Customer(baoZiPu);
Thread threadProduct=new Thread(product);
Thread threadCustomer=new Thread(customer);
threadProduct.start();
threadCustomer.start();
}
}
19.7.4多等待多唤醒案例
将代码改为:
//Main.java
package org.example;
import org.SelfDefinedThread.BaoZiPu;
import org.SelfDefinedThread.Product;
import org.SelfDefinedThread.Customer;
public class Main {
public static void main(String[] args) {
BaoZiPu baoZiPu=new BaoZiPu(0,false);
Product product=new Product(baoZiPu);
Customer customer=new Customer(baoZiPu);
new Thread(product).start();
new Thread(product).start();
new Thread(product).start();
new Thread(customer).start();
new Thread(customer).start();
new Thread(customer).start();
}
}
有如下可能:
- 六个线程同时开始抢占锁,假设生产线程1抢到锁并成功完成生产,此时
this.notify()
并未唤醒任何线程。 - 假设生产线程1、2、3依次抢到锁,并均由于
this.flag==true
而释放锁并进入阻塞。此时消费线程1、2、3开始抢占锁,假设消费线程1抢到锁完成消费并执行this.notify
唤醒生产线程1,此时消费线程1、2、3与生产线程1开始抢锁。 - 假设消费线程1、2、3依次抢到锁,则三者均被阻塞,生产线程1完成第二个包子的生产,并执行
this.notify()
唤醒线程,假设唤醒了生产线程2且生产线程2抢到了锁,此时生产线程2会继续执行this.wait()
后的代码,即使this.flag==true
,也会由于未经过if判断就跳出判断语句而继续向下执行,使得连续生产出第三个包子,而若之后生产线程2唤醒了生产线程3,则会出现相同的情况,连续生产出第四个包子。
解决方法1:将this.notify()
换成this.notifyAll()
,即一次性唤醒所有阻塞线程,此时消费线程就有机会抢占锁,即减小出现生产线程执行后再次唤醒生产线程而连续生产的概率,但并不能彻底解决问题:
解决方法2:将if
判断改为while
,即被唤醒线程应当再次进行判断,从而避免连续生产/消费的情况。并且,需要使用this.notifyAll()
,否则若唤醒同类线程,则会出现所有线程均被阻塞的情况。
19.7.5Lock锁的使用
Lock
接口提供了比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。它们允许更灵活的结构,可能具有完全不同的属性,并且可能支持多个关联的 Condition 对象。通常,锁提供对共享资源的独占访问:一次只有一个线程可以获取锁,并且对共享资源的所有访问都需要先获取锁。但是,某些锁可能允许并发访问共享资源,例如 ReadWriteLock 的读锁。接口方法:
一般的,使用Lock接口的实现类ReentrantLock
进行锁操作。以上文中买票案例为例:
package org.SelfDefinedThread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyTicket implements Runnable{
//定义100张票
static int ticket = 100;
//创建Lock对象
Lock lock=new ReentrantLock();
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买到票:" + ticket);
ticket--;
lock.unlock();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
由于无论中间代码是否抛出异常,锁最后都应释放,故可将lock的释放放在finally
代码块中:
package org.SelfDefinedThread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyTicket implements Runnable{
//定义100张票
static int ticket = 100;
//创建Lock对象
Lock lock=new ReentrantLock();
@Override
public void run() {
while(true){
try {
Thread.sleep(100L);
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "买到票:" + ticket);
ticket--;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}
}
synchronized
不管是同步代码块还是同步方法,都需要在结束一堆{}
之后释放锁对象,而lock是通过两个方法控制需要被同步的代码,更加灵活。
二十、集合
集合是一种长度可变的容器,具有以下特点:
- 只能存储引用数据类型的数据(基本类型会被自动装箱)。
- 长度可变。
- 集合中有大量方法,方便操作。
集合可分为单列集合(一个元素只有一个组成部分,如列表)、双列集合(一个元素有两个组成部分,如哈希表)。
20.1单列集合的介绍
- List接口:常用List接口实现类有ArrayList、LinkedList、Vector类。
- ArrayList:元素有序(存入顺序等于取出顺序),元素可重复且支持索引,线程不安全、底层数据结构是数组(可自动扩容)。
- LinkedList:元素有序(存入顺序等于取出顺序),元素可重复且支持索引(指支持索引操作),线程不安全、底层数据结构是双向链表(链表本质上无索引,但Java为其提供了很多根据索引操作的方法)。
- Vector:元素有序(存入顺序等于取出顺序),元素可重复且支持索引,线程安全(效率低,故不太常用)、底层数据结构是数组
- Set接口:常用Set接口实现类有HashSet、LinkedHashSet(是HashSet的子类)、TreeSet。
- HashSet:元素无序(存入顺序不一定等于取出顺序),元素唯一且无索引操作,线程不安全,底层数据结构是哈希表。
- LinkedHashSet:元素有序(存入顺序等于取出顺序),元素唯一且无索引操作,线程不安全,底层数据结构是哈希表+双向链表。
- TreeSet:可对元素进行排序,元素唯一且无索引,线程不安全,底层数据结构是红黑树。
20.2Collection接口的使用
使用方式:
Collection<E> 对象名=new 实现类对象<E>();
其中,<E>
称为泛型,其决定了集合中可存取数据的类型,且泛型中只能写引用数据类型,不指明时默认使用Object。注意,等号前面的泛型必须写,而等号后面的泛型可不写(JVM会根据前面的泛型推导出后面的泛型)。
Collection接口的常用方法:
方法名 | 功能 |
---|---|
boolean add(E e) | 将指定的元素添加到当前集合中,一般不接收返回值,因为add()一定会成功 |
boolean addAll(Collection<? extends E> c) | 将另一个集合的元素添加到当前集合中 |
void clear() | 清空当前集合中的所有元素 |
boolean contains(Object o) | 判断当前集合中是否包含指定的元素 |
boolean remove(Object o) | 将指定元素从集合中删除 |
boolean isEmpty() | 判断当前集合是否为空 |
int size() | 返回集合中元素的个数 |
Object[] toArray() | 把集合中的元素存储到数组中 |
package org.example;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class Main {
public static void main(String[] args) {
//创建一个集合对象
Collection<String> collection1=new ArrayList<>();
Collection<String> collection2=new ArrayList<>();
//add()
collection1.add("hello");
collection1.add("world");
collection2.add("java");
collection2.add("python");
//addAll()
collection1.addAll(collection2);
System.out.println(collection1); //[hello, world, java, python]
//clear()
collection2.clear();
System.out.println(collection2); //[]
//contains()
System.out.println(collection1.contains("C++")); //false
//isEmpty()
System.out.println(collection2.isEmpty()); //true
//remove()
collection1.remove("python");
System.out.println(collection1); //[hello, world, java]
//size()
System.out.println(collection1.size()); //3
//toArray():返回Object类型的数组,其中Arrays.toString()用于返回数组的字符串表示形式
Object[] arr=collection1.toArray();
System.out.println(Arrays.toString(arr)); //[hello, world, java]
}
}
20.3迭代器
20.3.1基本使用
迭代器(Iterator
)主要用于遍历集合。Collection
实现了Iterator
接口,并且提供了获取当前集合迭代器的方法:
public interface Collection<E> extends Iterable<E> {
...
Iterator<E> iterator();
...
}
而Iterator
接口提供了以下四种方法:
package org.example;
import java.util.ArrayList;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
//获取迭代器对象
Iterator<String> iterator=list.iterator();
while (iterator.hasNext()){
String s=iterator.next();
System.out.println(s);
}
}
}
注意,next()方法在获取元素时不应连续使用多次,如:
public class Main {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
//获取迭代器对象
Iterator<String> iterator=list.iterator();
while (iterator.hasNext()){
String s1=iterator.next();
System.out.println(s1);
String s2=iterator.next();
System.out.println(s1);
String s3=iterator.next();
System.out.println(s1);
}
}
}
即,使用next()
时应当保证当前迭代器中还有可操作的元素。
20.3.2源码分析
1.Iterator的获取方式
Iterator是一个接口,只能用来接收实现类对象,如:
Iterator iterator=list.iterator(); //list是ArrayList对象
实际接受的是ArrayList中的内部类Itr对象:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private class Itr implements Iterator<E> {}
public Iterator<E> iterator() {
return new Itr();
}
}
不同的集合中对于iterator接口有不同的实现类,实现了针对不同集合迭代器的不同功能。例如HashSet中的迭代器接口实现:
2.public E next()
查看ArrayList.java中定义的私有类:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
// prevent creating a synthetic constructor
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
...
}
}
cursor
:下一个元素的索引位置。lastRet = -1
:上一个访问元素的索引位置。
设size=3,则执行next()时cursor被赋值1,并返回数据缓冲区(elementData)中下标为0的元素,并将lastRet赋值为0,此时hasNext()返回true,表示仍有下一个元素可访问。以此类推,直到访问第三个元素时,cursor被赋值为3,lastRet赋值为2并返回下标为2的元素,此时hasNext()返回false,跳出while循环。
20.3.3并发修改异常
需求:定义一个集合,存储字符串"Python"、“Java”、“C”、“C++”,当遍历到"C"时,向集合中添加"C#"。
package org.example;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
//需求:定义一个集合,存储字符串"Python"、"Java"、"C"、"C++",当遍历到"C"时,向集合中添加"C#"
ArrayList<String> list=new ArrayList<>();
list.add("Python");
list.add("Java");
list.add("C");
list.add("C++");
//获取迭代器对象
Iterator<String> iterator=list.iterator();
while (iterator.hasNext()){
String s=iterator.next();
if(s.equals("C")){
list.add("C#");
}
}
}
}
查看源码:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
...
}
}
- modCount:实际操作次数。
- expectedModCount:预期操作次数。
查看add()源码:
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
可见,当调用add()函数时会对modCount
进行修改,而在获取迭代器对象时会对expectedModCount
进行赋值:int expectedModCount = modCount;
,故再调用迭代器对象的next()时会报错,此时只能重新获取集合的迭代器。即,迭代器是不可变的,当集合发生修改时,必须重新获取迭代器遍历集合。
在Java中提供了ListIterator<E>
接口,它允许程序员在任一方向遍历列表,在迭代期间修改列表,并获得迭代器在列表中的当前位置,提供方法有:
在ArrayList中有相应的实现类:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
private class ListItr extends Itr implements ListIterator<E> {...}
...
}
修改程序:
package org.example;
import java.util.ArrayList;
import java.util.ListIterator;
public class Main {
public static void main(String[] args) {
//需求:定义一个集合,存储字符串"Python"、"Java"、"C"、"C++",当遍历到"C"时,向集合中添加"C#"
ArrayList<String> list=new ArrayList<>();
list.add("Python");
list.add("Java");
list.add("C");
list.add("C++");
//获取迭代器对象
ListIterator<String> iterator= list.listIterator();
while (iterator.hasNext()){
String s=iterator.next();
if(s.equals("C")){
iterator.add("C#");
}
}
System.out.println(list);
}
}
20.4List接口
List
接口是Collection
的子接口,常用实现类:ArrayList、LinkedList、Vector。
20.4.1ArrayList集合
20.4.1.1基本介绍
ArrayList
是List接口的实现类,有如下特点:
- 元素有序:取出顺序等于存入顺序。
- 元素可重复
- 支持索引操作
- 线程不安全
- 底层数据结构:数组
常用方法
方法名 | 功能 |
---|---|
boolean add(E e) | 将指定的元素附加到此list的末尾。 |
void add(int index, E element) | 在此list中的指定位置插入指定元素。 |
E remove(int index) | 删除此list中指定位置的元素,返回值是移除的元素。 |
boolean remove(Object o) | 从此list中删除第一次出现的指定元素(如果存在),删除成功返回true。 |
E set(int index, E element) | 将指定索引位置上的元素修改为element |
E get(index) | 根据索引获取元素 |
int size() | 获取集合大小 |
package org.example;
import java.util.ArrayList;
import java.util.ListIterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
//1.add()
list.add("Python");
list.add("Java");
list.add("C");
list.add("C++");
//2.add()
list.add(2,"C#");
System.out.println(list);
//3.remove()
list.remove("C");
System.out.println(list);
//4.set()
list.set(1,"JavaScript");
System.out.println(list);
//5.get()
System.out.println(list.get(1));
//6.size()
System.out.println(list.size());
}
}
遍历集合的快捷键:
集合名.fori
注意,需要区分参数不同的同名方法:
此时应使用参数为Object o
的remove()函数。
20.4.1.2源码分析
1.构造函数
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
transient Object[] elementData;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//空参构造,构造一个容量为10的空列表
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//构造一个有指定容量的空列表
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//构造一个包含指定集合元素的列表,
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
-
ArrayList()
:构造一个初始容量为10的空数组。构造函数中将空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
赋值给底层数组elementData
(ArrayList()
的底层数据结构),而在执行add()
时才会。public boolean add(E e)
:修改次数加一,调用私有方法private void add(E e, Object[] elementData, int s)
,其中size表示集合元素个数。执行grow(int minCapacity)
时(minCapacity=1),会执行return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
,此时会返回一个大小为10的Object数组给elementData。public void add(int index, E element)
:执行rangeCheckForAdd(index)
时由于indexsize且size0而抛出异常IndexOutOfBoundsException
。
-
ArrayList(int initialCapacity)
:生成一个指定大小的Object属于组给elementData
。 -
ArrayList(Collection<? extends E> c)
:将传入的集合对象转换为数组,当数组长度为0时,将空数组赋值给elementData
(add()时再扩容),否则判断传入集合类型是否为ArrayList,决定时直接赋值给elementData
,还是调用Arrays.copyOf()
将数组赋值给elementData
。
对于Arrays.copyOf()
:
2.自动扩容机制
当ArrayList中存入第11个元素时,会使用Arrays.copyOf()
进行自动扩容。
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
在执行private Object[] grow(int minCapacity)
时,minCapacity等于11,oldCapacity=10,开始执行if语句。int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, oldCapacity >> 1);
,minCapacity - oldCapacity
=1,oldCapacity >> 1
相等于除2等于5。查看newLength()
源码:
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
int prefLength = oldLength + Math.max(minGrowth, prefGrowth);
if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
return prefLength;
} else {
// put code cold in a separate method
return hugeLength(oldLength, minGrowth);
}
}
int prefLength = oldLength + Math.max(minGrowth, prefGrowth);
后,prefLength=10+max
(1,5)=15并返回,再执行return elementData = Arrays.copyOf(elementData, newCapacity);
得到新数组,大小为15.
可见,当容量不够时会扩容到原来的1.5倍,若还不够,则扩容到加入元素后的元素个数大小。
20.4.2LinkedList
LinkedList
是List接口的实现类,有如下特点:
- 元素有序:取出顺序等于存入顺序。
- 元素可重复
- 支持索引操作
- 线程不安全
- 底层数据结构:双向链表,提供大量直接操作首尾元素的方法。
20.4.2.1基本使用
方法名 | 功能 |
---|---|
void addFirst(E e) | 在此list的开头插入指定的元素。 |
void addLast(E e) | 将指定的元素附加到此list的末尾。 |
E getFirst() | 返回此list中的第一个元素。 |
E getLast() | 返回此list中的最后一个元素。 |
E removeFirst() | 从此list中移除并返回第一个元素。 |
E removeLast() | 从此list中移除并返回最后一个元素。 |
E pop() | 从此list表示的堆栈中弹出一个元素(表头作为栈顶)。 |
void push(E e) | 将一个元素推入此list表示的堆栈中(表头作为栈顶)。 |
boolean isEmpty() | 判断列表是否为空。 |
20.4.2.2源码分析
//LinkedList主要成员
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0; //元素个数
transient Node<E> first; //第一个节点对象
transient Node<E> last; //最后一个节点对象
private static class Node<E> { //双向链表结点
E item; //数据域
Node<E> next; //后继指针
Node<E> prev; //前驱指针
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
...
}
1.add()
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
- 插入第一个节点时:
final Node<E> l = last;
后,I=null。执行final Node<E> newNode = new Node<>(l, e, null);
创建新节点,在Node(Node<E> prev, E element, Node<E> next)
中,prev=l,element=“a”,next=null。由于l == null
,故首节点指针first
指向newNode。
- 插入第二个节点时:l指向"a"节点(@728),执行语句创建新节点,在
Node(Node<E> prev, E element, Node<E> next)
中,prev=l,element=“b”,next=null。last指针指向"b"节点。由于前驱节点不为空,故l.next = newNode;
。
2.get()
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
当访问元素的索引小于元素个数的一半时即正向查找,否则逆向查找。
20.5Collections集合工具类
Collections.java
是集合工具类,其构造函数是私有方法,且其他方法都是静态的,可通过类名进行直接调用。常用方法:
方法名 | 函数名 |
---|---|
static booleanaddAll(Collection<? super T> c, T… elements) | 将所有指定元素添加到指定集合 |
static void shuffle(List<?> list) | 将集合中的元素顺序打乱 |
static <T extends Comparable<? super T>> void | 将集合中的元素按默认规则(ASCII码表)排序 |
static void sort(List list, Comparator<? super T> c) | 根据指定比较器对指定列表进行排序。 |
package org.example;
import java.util.ArrayList;
import java.util.Collections;
public class Main {
public static void main(String[] args) {
ArrayList<String> arrayList=new ArrayList<>();
//addAll()
Collections.addAll(arrayList,"a","b","c","d","e");
System.out.println(arrayList);
//shuffle()
Collections.shuffle(arrayList);
System.out.println(arrayList);
//sort():使用默认规则排序
Collections.sort(arrayList);
System.out.println(arrayList);
}
}
static <T> void sort(List<T> list, Comparator<? super T> c)
:
Comparator是比较器接口,其中提供int compare(T o1, T o2);
进行比较,可简记为:
- o1-o2:升序排列。
- o2-o1:降序排列。
创建JavaBean类Person,其中包含属性name(String)、age(int)。
package org.example;
import org.pojo.Person;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Main {
public static void main(String[] args) {
ArrayList<Person> arrayList=new ArrayList<>();
Collections.addAll(arrayList,new Person("张三",18),new Person("李四",20),new Person("王五",19));
Collections.sort(arrayList, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge()-o2.getAge();
}
});
System.out.println(arrayList);
}
}
事实上,String底层实现了Comparable接口,重写了比较方法。
在Arrays中提供了静态方法:
static <T> List<T> asList(T... a)
//直接指定元素,转存到List集合中
List<String> list= Arrays.asList("a","b","c","d","e");
20.6泛型
20.6.1基本介绍
泛型用于统一数据类型,防止出现数据类型转换异常,并且,定义带有泛型的类,将来使用的时候可自由选择数据类型,使得代码更加灵活。使用格式:
<>
- 泛型中的类型必须是引用类型。
- 若泛型不写,则默认类型为Object。
package org.example;
import java.util.*;
public class Main {
public static void main(String[] args) {
ArrayList list=new ArrayList();
list.add("1");
list.add(1);
list.add("abc");
list.add(2.5);
list.add(true);
//获取元素为String类型的字符串长度
for(Object o : list){
if(o instanceof String){
String s=(String)o;
System.out.println(s.length());
}
}
}
}
20.6.2含有泛型的类
定义方式:
public class 类名<E>{ //在new对象时确定类型
...
}
例:
package org.myClass;
import java.util.Arrays;
public class MyArrayList<E> {
//定义一个数组存储数据,长度为10
private Object[] elementData=new Object[10];
//记录集合元素个数
int size;
//添加元素
public void add(E e){
if(size==elementData.length){
//自动扩容为2倍
Object[] newElementData=new Object[elementData.length*2];
System.arraycopy(elementData,0,newElementData,0,elementData.length);
elementData=newElementData;
}
elementData[size++]=e;
}
//获取元素
public E get(int index){
if(index<0||index>=size){
throw new IndexOutOfBoundsException("索引越界");
}
return (E)elementData[index];
}
//打印元素
@Override
public String toString() {
return "MyArrayList{" +
"elementData=" + Arrays.toString(elementData) +
", size=" + size +
'}';
}
}
20.6.3含有泛型的方法
定义方式:
修饰符 <E> 返回值类型 方法名(参数列表){
//当所在类声明了<E>时可直接使用,否则应在返回值前声明泛型
函数体;
}
例:
package org.myClass;
import java.util.ArrayList;
public class MyListUtils {
//定义一个静态方法addAll,添加多个集合的元素
public static <E> void addAll(ArrayList<E> list,E...e){
for (E e1 : e) {
list.add(e1);
}
}
}
20.6.4含有泛型的接口
定义方式:
public interface 接口名<E>{
...
}
例:
//MyList.java
package org.MyInterface;
public interface MyList <E>{
public void add(E e);
}
//MyArrayList.java
package org.myClass;
import org.MyInterface.MyList;
public class MyArrayList<E> implements MyList<E> {
//定义一个数组存储数据,长度为10
private Object[] elementData=new Object[10];
//记录集合元素个数
int size;
//添加元素
public void add(E e){
if(size==elementData.length){
//自动扩容为2倍
Object[] newElementData=new Object[elementData.length*2];
System.arraycopy(elementData,0,newElementData,0,elementData.length);
elementData=newElementData;
}
elementData[size++]=e;
}
}
在实现MyList接口时,若使用implements MyList<E>
,则必须重写void add(E e);
(在创建对象时指定数据类型),而若使用implements MyList<String>
,则必须重写void add(String e);
。
20.6.5泛型的上下限
在 Java 中,类和数组之间的对象关系是可以继承的,比如 Dog extends Animal,那么 Animal[] 与 Dog[] 就是兼容的。但是集合之间却不存在这种关系,也就是说 List 不是List 的父类,他们之间没有任何关系。那么为了建立两个集合之间的联系,就需要用到通配符。
Java提供了泛型通配符?
表示未知类型,通配符可作为参数、字段或局部变量的类型;有时也可以作为返回类型。另外,通配符永远不会用作调用泛型方法、创建泛型类或超类型实例的类型参数。
public static void print(ArrayList<?> list){
for (Object o:list) {
System.out.println(o);
}
}
<T>
与<?>
的区别在于,<T>
可将T作为一种数据类型定义变量、作返回值类型等,而<?>
仅仅表示无法确定当前泛型类型(即方法本身并不依赖于类型参数),但<?>
可配合用于设置泛型上下限,限定传入泛型的范围。
<? extends E>
:确定上限,接收E类型或E的子类型对象。<? super E>
:确定下限,接收E类型或E的父类型对象。
package org.example;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
public class Main {
public static void main(String[] args) {
//子父类关系:Integer->Number->Object,String->Object
ArrayList<Integer> list1=new ArrayList<>();
ArrayList<String> list2=new ArrayList<>();
ArrayList<Number> list3=new ArrayList<>();
ArrayList<Object> list4=new ArrayList<>();
//get1()
get1(list1);
//get1(list2);报错
get1(list3);
//get1(list4);报错
//get2()
//报错get2(list1);
//报错get2(list2);
get2(list3);
get2(list4);
}
//只能接收Number及其子类类型的集合
public static void get1(Collection<? extends Number> collection){}
//只能接收Number及其父类类型的集合
public static void get2(Collection<? super Number> collection){}
}
20.7Set接口
Set接口并没有对Collection接口进行功能上的扩充,而且所有的Set集合底层都是依赖Map实现。查看HashSet源码:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = HashMap.newHashMap(Math.max(c.size(), 12));
addAll(c);
}
...
}
可见,创建HashSet的本质是创建HashMap,而LinkedHashSet、TreeSet等同理。Set与Map密切相关,Map的遍历需先变成单列集合,并且只能变成Set集合。
20.7.1HashSet
HashSet是Set接口的实现类,有如下特点:
- 元素唯一。
- 元素无序(存入顺序不等于取出顺序)。
- 元素无索引,不支持索引操作。
- 线程不安全。
- 底层数据结构:哈希表。
在JDK8之前,哈希表由数组和链表组成,由于链表查询效率较低,JDK8之后哈希表由数组、链表、红黑树共同组成,加快查询效率。HashSet的使用方法与Collections基本一致,遍历时可使用增强for(由于不具备索引,故不能使用普通for循环遍历)、迭代器。
package org.example;
import java.util.*;
public class Main {
public static void main(String[] args) {
HashSet<String>hashSet=new HashSet<>();
hashSet.add("a");
hashSet.add("b");
hashSet.add("c");
hashSet.add("a");
System.out.println(hashSet); //[a, b, c]:元素唯一
//迭代器遍历
Iterator<String> iterator=hashSet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
//增强for遍历
for(String s:hashSet){
System.out.println(s);
}
}
}
20.7.2LinkedHashSet
LinkedHashSet
是HashSet
的子类,由如下特点:
- 元素唯一。
- 元素有序(存入顺序等于取出顺序)。
- 无索引,不支持索引操作。
- 线程不安全。
- 底层数据结构:哈希表+双向链表。
LinkedHashSet
的方法与HashSet
一致,不再演示。
20.7.3哈希值
哈希值是由计算机算出的十进制数,可看作是对象的地址值,获取对象的哈希值时使用的是Object中的方法public native int hashCode();
。创建含有name(String)、age(int)的JavaBean类Person,当不重写toString()方法时打印的是对象的地址值:
查看Object类中的toString()方法:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
即默认打印"类路径@十进制哈希值"。而使用:
package org.example;
public class Main {
public static void main(String[] args) {
String s1="abc";
String s2=new String("abc");
System.out.println(s1.hashCode()); //96354
System.out.println(s2.hashCode()); //96354
}
}
虽然s1与s2地址值不同,但二者哈希值相同,这是因为String底层重写了hashCode()方法:
public int hashCode() {
int h = hash;
if (h == 0 && !hashIsZero) {
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true;
} else {
hash = h;
}
}
return h;
}
可在Person类中重写hashCode()方法:
@Override
public int hashCode() {
return Objects.hash(name, age);
}
此时具有相同属性值的Person对象有了相同的哈希值(不重写时默认计算对象的哈希值,重写后计算的是对象内容的哈希值)。
注意,字符串内容不同,但哈希值仍可能相同(因为哈希值本质是采用哈希函数算出,并不代表真正的地址值,可能出现同义词):
package org.example;
public class Main {
public static void main(String[] args) {
String s1="通话";
String s2="重地";
System.out.println(s1.hashCode()); //1179395
System.out.println(s2.hashCode()); //1179395
}
}
- 哈希值不一样,内容肯定不同。
- 哈希值相同,内容有可能不同。
20.7.4字符串哈希算法
字符串底层使用字符数组value[]进行存储,其重写的hashCode()方法如下:
//String.java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
@Stable
private final byte[] value;
private int hash; // Default to 0
public int hashCode() {
int h = hash;
if (h == 0 && !hashIsZero) {
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true;
} else {
hash = h;
}
}
return h;
}
...
}
//StringUTF16.java
public static int hashCode(byte[] value) {
return switch (value.length) {
case 0 -> 0;
case 2 -> getChar(value, 0);
default -> ArraysSupport.vectorizedHashCode(value, 0, value.length >> 1, 0, ArraysSupport.T_CHAR);
};
}
一直步入到计算哈希值的方法:
//ArraysSupport.java
public static int utf16hashCode(int result, byte[] value, int fromIndex, int length) {
int end = fromIndex + length;
for (int i = fromIndex; i < end; i++) {
result = 31 * result + JLA.getUTF16Char(value, i);
}
return result;
}
使用的JDK22底层进行了包装,而查看JDK17版本的源码:
public static int hashCode(byte[] value){
int h=0;
for(byte v:value){
h=31*h+(v&0xff);
}
return h;
}
其中,0xff
对应255,任何数据和255作&运算都是原值,对于"abc"需执行三次循环:
- h=31*0+97=97
- h=31*97+98=3105
- h=31*3105+99=96354
最后得到"abc"的哈希值96354。之所以使用31,是在因为31是一个质数,并且通过大量的统计,使用31可以尽量降低内容不一致但哈希值一致(哈希冲突)的可能性。
20.7.5HashSet去重复过程说明
HashSet去重复过程为,
- 先计算元素的哈希值(需重写hashCode方法)并进行比较,若哈希值不同则直接存储,否则再比较内容(重写equals方法),若内容不同则直接存储。
- 若哈希值相同、内容相同,则去重复。
对于自定义类型,也需要重写hashCode与equals方法后才能实现去重。
总结:
- 若HashSet存储自定义类型,则需重写hashCode与equals方法,让HashSet比较属性的哈希值以及属性的内容实现去重。
- 若不重写hashCode与equals方法,则默认调用的是Object中的hashCode与equals方法,即比较的是对象的地址(哈希值),此时即使对象的属性值相同,也无法去重。
20.8双列集合
Map是双列集合的顶级接口,存储元素为键值对类型。
HashMap特点:
- 实现了Map接口。
- key值唯一,value可重复。
- 无序。
- 无索引,不支持索引操作。
- 线程不安全。
- 可存储null键、null值。
- 底层数据结构:哈希表。
LinkedHashMap特点:
- 继承自HashMap。
- key值唯一,value可重复。
- 有序。
- 无索引,不支持索引操作。
- 线程不安全。
- 可存储null键、null值。
- 底层数据结构:哈希表+双向链表。
Hashtable特点:
- 实现了Map接口。
- key值唯一,value可重复。
- 无序。
- 无索引,不支持索引操作。
- 线程安全。
- 不可存储null键、null值。
- 底层数据结构:哈希表。
Properties(主要和配置文件结合使用)特点:
- 继承自Hashtable。
- key值唯一,value可重复,且都是String类型(故创建时无需指定泛型)。
- 无序。
- 无索引,不支持索引操作。
- 线程安全。
- 不可存储null键、null值。
- 底层数据结构:哈希表。
TreeMap特点:
- 实现了Map接口。
- key值唯一,value可重复,可对key进行排序。
- 无序。
- 无索引,不支持索引操作。
- 线程不安全。
- 不可存储null键、null值。
- 底层数据结构:红黑树。
20.8.1HashMap
HashMap是Map的实现类,key唯一且value,当新增键值对key重复时,则会覆盖原key键值对。
- 实现了Map接口。
- key值唯一,value可重复。
- 无序。
- 无索引,不支持索引操作。
- 线程不安全。
- 可存储null键、null值。
- 底层数据结构:哈希表。
方法名 | 功能 |
---|---|
V put(K key, V value) | 添加键值对,返回的是被覆盖的value(不存在覆盖则返回null) |
V remove(Object key) | 根据键值删除键值对,返回的是被删除的Value |
V get(Object key) | 根据key获取value |
boolean containsKey(Object key) | 判断集合中是否包含指定的key |
Collection values() | 获取集合中所有的value,并存储到Collection集合中 |
Set keySet() | 将Map中的键key转存到set集合中 |
Set<Map.Entry<K,V>> entrySet() | 将Map中的键值对转存到set集合中 |
package org.example;
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String,String>hashMap=new HashMap<>();
//put()
hashMap.put("name","张三");
hashMap.put("age","18");
hashMap.put("sex","Male");
System.out.println(hashMap); //{sex=Male, name=张三, age=18}
System.out.println(hashMap.put("name","李四")); //返回被覆盖的value:张三
//remove()
System.out.println(hashMap.remove("sex")); //返回sex对应的value:Male
System.out.println(hashMap); //{name=李四, age=18}
//get()
System.out.println(hashMap.get("name")); //李四
//values()
System.out.println(hashMap.values()); //[李四, 18]
}
}
HashMap的第一种遍历方式:获取key集合,根据key再获取value。
package org.example;
import java.util.HashMap;
import java.util.Set;
public class Main {
public static void main(String[] args) {
HashMap<String,String>hashMap=new HashMap<>();
hashMap.put("name","张三");
hashMap.put("age","18");
hashMap.put("sex","Male");
Set<String> keySet = hashMap.keySet();
for(String key:keySet){
System.out.println(key+":"+hashMap.get(key));
}
}
}
HashMap的第二种遍历方式:获取键值对集合,直接遍历。在Map接口中提供了内部接口Map.Entry,其提供了getKey()与getValue()两种方法用于获取键、值集合。
package org.example;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Main {
public static void main(String[] args) {
HashMap<String,String>hashMap=new HashMap<>();
hashMap.put("name","张三");
hashMap.put("age","18");
hashMap.put("sex","Male");
Set<Map.Entry<String,String>> entrySet=hashMap.entrySet();
for (Map.Entry<String,String> entry:entrySet){
System.out.println(entry.getKey()+":"+entry.getValue());
}
}
}
20.8.2LinkedHashMap
LinkedHashMap,继承自HashMap,可使用父类的方法,有如下特点:
- key值唯一,value可重复。
- 有序。
- 无索引,不支持索引操作。
- 线程不安全。
- 可存储null键、null值。
- 底层数据结构:哈希表+双向链表。
类中增加了如下方法:
20.8.3Map存储自定义对象时的去重方式
Map用自定义对象作为键时,需重写hashCode和equals方法才能去重,去重过程与set一致(set中的元素底层保存在Map当中)。
20.9哈希表存储数据去重复过程
哈希表存储数据去重复过程:
- 先比较元素的哈希值(重写hashCode方法),再比较内容(重写equals方法)。
- 若哈希值不同,则证明内容不同,可直接存入。
- 若哈希值相同,内容不同(发生哈希冲突),可直接存入。
- 若哈希值相同,内容也相同,则需去重复。
- 数组的默认长度为16(第一次执行put操作时才初始化数组大小为16)。
- 存入数据"abc":
- 计算"abc"的哈希值为96354,并将其再次哈希算出96355.
- 与其他数据进行比较,并无相同哈希值的数据,发现可以存储后用96355作与运算计算存储位置(假设是索引3处)。
- 存入数据"通话":
- 计算"通话"的哈希值为1179395,并将其再次哈希算出1179410.
- 与其他数据进行比较,并无相同哈希值的数据,发现可以存储后用1179410作与运算计算存储位置(假设是索引2处)。
- 存入数据"重地":
- 计算"通话"的哈希值为1179395,并将其再次哈希算出1179410.
- 与其他数据进行比较,发现有相同哈希值的数据但内容不同可存储,用1179410作与运算计算出存储位置为索引2处,此位置有数据存储故以链表方式存储。
当链表过长时,就会将链表转为红黑树进行存储。细节如下:
- 哈希表中的数组默认长度为16,但第一次put时才会将数组初始化为16的数组。
- 哈希表中有加载因子0.75F,表示当数组存储达到75%时会将数组扩容为两倍。
- 若链表长度达到8,并且数组容量大于等于64时,链表会自动转为红黑树。
- 若删除元素,使得红黑树元素个数小于6,则红黑树会转回链表。
相关变量(面试考点):
DEFAULT_INITIAL_CAPACITY
=16:HashMap默认容量。DEFAULT_LOAD_FACTOR
= 0.75f:默认加载因子。TREEIFY_THRESHOLD
= 8:链表最大长度默认值。MIN_TREEIFY_CAPACITY
= 64:链表被树化的最小数组容量。threshold
=容量*0.75:数组扩容临界值,第一次扩容在数组容量需大于12时。
20.10哈希表有无索引、有无序详解
1.哈希表中有数组的存在但为什么说没有索引?
哈希表中虽然有数组,但是set和map却没有索引,因为存数据的时候可能在同一个索引下形成链表,此时无法仅通过索引就获取元素。故而取消了索引操作的机制。
2.为什么说HashMap是无序的,而LinkedHashMap是有序的
因为HashMap底层哈希表为单向链表,而LinkedHashMap底层在哈希表的基础上加了一条双向链表。上图(1)中存储顺序为1、2、3,但单向链表的查找是从索引零处的链表依次向后查找,即一条链表一条链表向后遍历。上图(2)中采用双向链表存储,存储顺序为1、2、3、4,遍历查找顺序等于存储顺序。
20.11TreeSet和TreeMap
20.11.1TreeSet
TreeSet是Set的实现类,有如下特点:
- 可对元素进行排序,且元素唯一。
- 无索引。
- 不能存储null。
- 线程不安全。
- 底层数据结构:红黑树。
20.11.1TreeMap
TreeMap是Map的实现类,有如下特点:
- 可对key进行排序,且key唯一。
- 无索引。
- 线程不安全。
- 不能存储null。
- 底层数据结构:红黑树。
20.12Hashtable和Vector
略
20.13Properties属性集
Properties继承自HashTable,有如下特点:
- key唯一,且key与value默认为String。
- 无索引、无序。
- 线程安全。
- 不能存储null。
- 底层数据结构:哈希表。
方法名 | 功能 |
---|---|
Object setProperty(String key, String value) | 存储键值对 |
String getProperty(String key) | 获取key对应的值 |
Set stringPropertyNames() | 获取所有的key并存储到set集合中,相当于keySet方法 |
void load(InputStream inStream) | 将流中的数加载到Properties集合中 |
package org.example;
import java.util.Properties;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Properties properties=new Properties();
//setProperty(String key, String value)
properties.setProperty("username","root");
properties.setProperty("password","123456");
System.out.println(properties); //{password=123456, username=root}
//getProperty(String key)
System.out.println("username:"+properties.getProperty("username"));
System.out.println("password:"+properties.getProperty("password"));
//stringPropertyNames()
System.out.println(properties.stringPropertyNames()); //[password, username]
Set<String> set=properties.stringPropertyNames();
for(String s:set){
System.out.println(s+":"+properties.getProperty(s));
}
}
}