01章 入门
卸载与安装
卸载
删除java的安装目录
删除环境变量配置JAVA_HOME
删除环境变量中path下关于java的目录
命令行验证java -version 出现命令错误表示下载成功
安装
百度搜索jdk8,找到下载地址
同意协议
下载电脑的对应版本
双击安装jdk的exe安装程序
记住安装的路径
配置环境变量JAVA_HOME
配置path
命令行窗口 java -version 验证命令,出现java的版本信息,安装成功
jdk、jre、jvm
jdk包含jre,jre包含jvm
jdk(Java Development Kit java开发者工具):JRE的超集,包含编译器和调试器等用于程序开发的文件
jre(Java Runtime Environment java运行环境):Java虚拟机、库函数、运行Java应用程序和Applet所必须文件
jvm(Java Virtual Machine):JVM是一种规范,可以使用软件来实现,也可以使用硬件来实现,就是一个虚拟的用于执行bytecodes字节码的计算机 。他也定义了指令集、寄存器集、结构栈、垃圾收集堆、内存区域。
java程序运行机制
编译型:java文件编译为class文件
解释型:解释一句运行一句
java使用编译型和解释型结合的方式运行
1.一个类中,成员变量(属性)有默认值,引用数据类型是null值,基本数据类型就是本身默认值。方法中,局部变量,没有默认值
02章 编程基础
标识符
java关键字
注意点
所有的标识符都应该以字母(A-Z 或者 a-z),美元符($)、或者下划线(_)开始
首字符不能时数字和特殊字符
首字符之后可以是字母(A-Z 或者 a-z),美元符($)、下划线(_)或数字的任何字符组合
不能使用关键字作为变量名或方法名。
标识符是大小写敏感的
合法标识符举例:age、$salary、_value、__1_value
非法标识符举例:123abc、-salary、#abc
数据类型
基本数据类型
public static void main(String[] args) {
//长整型
long i2=998877665544332211L;
//浮点型
double d1=3.5; //双精度
float f2=3.5f; //单精度
//布尔类型 boolean true真/false假
boolean isPass=true;
boolean isOk=false;
//单字符
char f='女';
char m='男';
}
引用数据类型
除了基本的数据类型之外,其它的都是引用数据类型
整型拓展
十进制整数,如:99, -500, 0。
八进制整数,要求以 0 开头,如:015。
十六进制数,要求 0x 或 0X 开头,如:0x15
//整型
int i=10;
int i2=010;
int i3=0x10;
System.out.println(i); //10
System.out.println(i2); //8
System.out.println(i3); //16
进制转换:
二进制转换成十进制:按权相加
十进制转换成二进制:整数部分除2取余,倒序排列,小数部分乘2取整,正序排列
浮点型拓展
浮点类型float, double的数据不适合在不容许舍入误差的金融计算领域。
如果需要进行不产生舍入误差的精确数字计算,需要使用BigDecimal类。
public static void main(String[] args) {
float f = 0.1f;
double d = 1.0/10;
System.out.println(f==d); //false
float d1 = 2131231231f;
float d2 = d1+1;
// d1==d2 true
if(d1==d2){
System.out.println("d1==d2");
}else{
System.out.println("d1!=d2");
}
}
主要理由:
由于字长有限,浮点数能够精确表示的数是有限的,因而也是离散的。
浮点数一般都存在舍入误差
很多数字无法精确表示,其结果只能是接近,但不等于;二进制浮点数不能精确的表示0.1,0.01,0.001这样
10的负次幂。
并不是所有的小数都能可以精确的用二进制浮点数表示
最好完全避免使用浮点数比较
字符型拓展
单引号用来表示字符常量。例如‘A’是一个字符,它与“A”是不同的,“A”表示一个字符串。
char 类型用来表示在Unicode编码表中的字符。
Unicode编码被设计用来处理各种语言的所有文字,它占2个字节,可允许有65536个字符;
【科普:2字节=16位 2的16次方=65536,我们用的Excel原来就只有这么多行,并不是无限的】
public static void main(String[] args) {
char c1 = 'a';
char c2 = '中';
System.out.println(c1);
System.out.println((int) c1); //97
System.out.println(c2);
System.out.println((int) c2); //20013
}
Unicode具有从0到65535之间的编码,他们通常用从’u0000’到’uFFFF’之间的十六进制值来表示(前缀为u表示Unicode)
// \u代表转义,将数字转义为Unicode编码的对应字符
char c3 = '\u0061';
System.out.println(c3); //a
转义字符
类型转换
低 ---------------------------------------- >高
byte,short,char—> int —> long—> float —> double
数据类型转换必须满足如下规则:
不能对boolean类型进行类型转换。
不能把对象类型转换成不相关类的对象
在把容量大的类型转换为容量小的类型时必须使用强制类型转换
转换过程中可能导致溢出或损失精度
int i =128;
byte b = (byte)i; // -128
// 因为 byte 类型是 8 位,最大值为127,所以当 int 强制转换为 byte 类型时,值 128 时候就会导致溢出。
浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入
(int)23.7 == 23;
(int)-45.89f == -45
常见错误和问题
操作比较大的数时,要留意是否溢出,尤其是整数操作时;
public static void main(String[] args) {
int money = 1000000000; //10亿
int years = 20;
int total = money*years; //返回的是负数
//返回的仍然是负数。默认是int,因此结果会转成int值,再转成long。但是已经发生了数据丢失
long total1 = money*years;
//先将一个因子变成long,整个表达式发生提升。全部用long来计算。
long total2 = money*((long)years);
System.out.println(total);
System.out.println(total1);
System.out.println(total2);
}
L和l 的问题:
不要命名名字为l的变量
long类型使用大写L不要用小写。
public static void main(String[] args) {
int l = 2;
long a = 23451l;
System.out.println(l+1); //3
System.out.println(a); //23451
}
JDK7扩展
JDK7新特性:下划线分隔符
在实际开发和学习中,如果遇到特别长的数字,读懂它令人头疼!JDK7为我们提供了下划线分隔符,可
以按照自己的习惯进行分割。
int b = 1_2234_5678; // 我们很容易就知道这是1亿2234万5678啦! 非常符合国人的习惯
变量和常量
变量
变量是什么:就是可以变化的量!
我们通过变量来操纵存储空间中的数据,变量就是指代这个存储空间!空间位置是确定的,但是里面放置什么值不确定
Java是一种强类型语言,每个变量都必须声明其类型。
Java变量是程序中最基本的存储单元,其要素包括变量名,变量类型和作用域。
变量在使用前必须对其声明, 只有在变量声明以后,才能为其分配相应长度的存储单元
常量
常量(Constant):初始化(initialize)后不能再改变值!不会变动的值
所谓常量可以理解成一种特殊的变量,它的值被设定后,在程序运行过程中不允许被改变
final 常量名=值;
final double PI=3.14; final String LOVE="hello";
运算符
算术运算符: +,-,*,/,%,++,--
赋值运算符 =
关系运算符: >,<,>=,<=,==,!= instanceof
逻辑运算符: &&,||,!
位运算符: &,|,^,~ , >>,<<,>>> (了解!!!)
条件运算符 ?:
扩展赋值运算符:+=,-=,*=,/=
二元运算符
整数运算:
如果两个操作数有一个为Long(或float), 则结果也为long(或float)。没有long(或float)时,结果为int。即使操作数全为shot,byte,结果也是int.
浮点运算:
如果两个操作数有一个为double, 则结果为double。只有两个操作数都是float, 则结果才为float.
取模运算
就是我们小学的取余; 5%3 余 2
其操作数可以为浮点数,一般使用整数。如:5.9%3.9=2.000000004
要点:
负数%负数=负数;
负数%正数=负数;
正数%负数=正数;
public static void main(String[] args) {
System.out.println(9 % 4); //1
System.out.println(-9 % -4); //-1
System.out.println(-10 % 4); //-2
System.out.println(9 % -4); //1
}
【注:一般都是正整数运算,进行结果的判断】
一元运算符
自增(++)自减(--)运算符
++在前,先自增,在使用
++在后,先使用,在自增
--亦然
public static void main(String[] args) {
int a = 3;
int b = a++; //执行完后,b=3。先给b赋值,再自增。
int c = ++a; //执行完后,c=5。先自增,再给b赋值
}
注意:java中的乘幂处理
public static void main(String[] args) {
int a = 3^2; //java中不能这么处理, ^是异或符号。
double b = Math.pow(3, 2);
}
Math类提供了很多科学和工程计算需要的方法和常数。特殊的运算都需要运用到方法!
逻辑运算符
逻辑与:&&,逻辑或:||,逻辑非:!
逻辑与和逻辑或采用短路的方式。从左到右计算,如果确定值则不会再计算下去
位运算符
应用于整数类型(int),长整型(long),短整型(short),字符型(char),和字节型(byte)等类型。位运算符作用在所有的位上
// 可以理解成1为真0为假
A = 0011 1100
B = 0000 1101
-----------------
A&b = 0000 1100 // 都为1才是1
A | B = 0011 1101 // 都是0,才为0
A ^ B = 0011 0001 // 异或 值相同为0 不同为1
~A= 1100 0011 // 按位取反 0变1 1变0
右移一位相当于除2取商
左移一位相当于乘2
解释一下,在系统中运算是以二进制的形式进行的。相比来说俩个二进制数相乘运算比移位运算慢一些。
扩展运算符
字符串连接符
“+” 运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接
三目条件运算符
public static void main(String[] args) {
int score = 80;
String type = score < 60 ? "不及格" : "及格";
System.out.println("type= " + type);
}
运算符优先级
大家不需要去刻意的记住,表达式里面优先使用小括号来组织!!方便理解和使用,不建议写非常冗余的代码运算!
包机制
为了更好地组织类,Java 提供了包机制,用于区别类名的命名空间。
包的本质就是文件夹
JavaDoc
稍微解释一下:以 /** 开始,以 */ 结束
@author 作者名
@version 版本号
@since 指明需要最早使用的jdk版本
@param 参数名
@return 返回值情况
@throws 异常抛出情况
/** 这是一个Javadoc测试程序
* @author Kuangshen
* @version 1.0
* @since 1.5
* */
public class HelloWorld {
public String name;
/**
* @param name 姓名
* @return 返回name姓名
* @throws Exception 无异常抛出
* */
public String function(String name) throws Exception{
return name;
}
}
命令行生成Doc
用户交互Scanner
Scanner对象
两者区别:
流程结构
顺序结构
JAVA的基本结构就是顺序结构,除非特别指明,否则就按照顺序一句一句执行
顺序结构在程序流程图中的体现就是用流程线将程序框自上而地连接起来,按顺序执行算法步骤
选择结构
if单选择结构
if(布尔表达式){
//如果布尔表达式为true将执行的语句
}
if双选择结构
if(布尔表达式){
//如果布尔表达式的值为true
}else{
//如果布尔表达式的值为false
}
if多选择结构
if(布尔表达式 1){
//如果布尔表达式 1的值为true执行代码
}else if(布尔表达式 2){
//如果布尔表达式 2的值为true执行代码
}else if(布尔表达式 3){
//如果布尔表达式 3的值为true执行代码
}else {
//如果以上布尔表达式都不为true执行代码,也可以忽略
}
// 只能执行一个代码块
嵌套的if结构
if(布尔表达式 1){
////如果布尔表达式 1的值为true执行代码
if(布尔表达式 2){
////如果布尔表达式 2的值为true执行代码
}
}
switch多选择结构
多选择结构还有一个实现方式就是switch case 语句
switch case 语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支。
switch(expression){
// break 如果没有,会发生case穿透现象
case value :
//语句
break; //可选
case value :
//语句
break; //可选
...
default : //可选
//语句
}
switch case 语句有如下规则:
switch 语句中的变量类型可以是: byte、short、int 或者 char。从 Java SE 7 开始,switch 支持字符串 String 类型
case 标签必须为字符串常量或字面量 。
default 分支不需要 break 语句。
循环结构
while 循环
while( 布尔表达式 ) {
//循环内容
}
do...while 循环
do {
//代码语句
}while(布尔表达式);
while和do-while的区别:
while先判断后执行。dowhile是先执行后判断
do...while总是保证循环体会被至少执行一次!这是他们的主要差别。
for循环
for(初始化; 布尔表达式; 更新) {
//代码语句
}
public static void main(String[] args) {
int a = 1; //初始化
while(a<=100){ //条件判断
System.out.println(a); //循环体
a+=2; //迭代
}
System.out.println("while循环结束!");
for(int i = 1;i<=100;i++){ //初始化//条件判断 //迭代
System.out.println(i); //循环体
}
System.out.println("while循环结束!");
}
我们发现,for循环在知道循环次数的情况下,简化了代码,提高了可读性。我们平时用到的最多的也是我们的for循环
增强for循环
Java5 引入了一种主要用于数组或集合的增强型 for 循环
for(声明语句 : 表达式){
//代码句子
}
public static void main(String[] args) {
int [] numbers = {10, 20, 30, 40, 50};
for(int x : numbers ){
System.out.print( x );
System.out.print(",");
}
System.out.print("\n");
String [] names ={"James", "Larry", "Tom", "Lacy"};
for( String name : names ) {
System.out.print( name );
System.out.print(",");
}
}
break & continue & goto
break关键字
break 主要用在循环语句或者 switch 语句中,用来跳出整个语句块。
break 跳出最里层的循环,并且继续执行该循环下面的语句。
switch 语句中break在上面已经详细说明了,如果有疑惑可以回头看switch多选择结构小节
public static void main(String[] args) {
int i=0;
while (i<100){
i++;
System.out.println(i);
if (i==30){
break;
}
}
}
continue 关键字
continue 适用于任何循环控制结构中。作用是让程序立刻跳转到下一次循环的迭代
在 for 循环中,continue 语句使程序立即跳转到更新语句(例如常见的i++语句)。
在 while 或者 do...while 循环中,程序立即跳转到布尔表达式的判断语句
两者区别
break在任何循环语句的主体部分,均可用break控制循环的流程。用于强行退出循环,不执行循环中剩余的语句。
break语句也在switch语句中使用
continue 语句用在循环语句体中,用于终止某次循环过程,即跳过循环体中尚未执行的语句,接着进行下一次是否执行循环的判定
goto关键字
了解即可
goto关键字很早就在程序设计语言中出现。尽管goto仍是Java的一个保留字,但并未在语言中得到正式使用;Java没有goto。然而,在break和continue这两个关键字的身上,我们仍能看出一些goto的影子---带标签的break|continue。
“标签”是指后面跟一个冒号的标识符,例如:label:
对Java来说唯一用到标签的地方是在循环语句之前。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环,由于break和continue关键字通常只中断当前循环,但若随同标签使用,它们就会中断到存在标签的地方。
// 【看不懂没关系,只是了解一下即可,知道goto这个保留字和标签的写法】
public static void main(String[] args) {
int count = 0;
outer: for (int i = 101; i < 150; i ++) {
for (int j = 2; j < i / 2; j++) {
if (i % j == 0){
continue outer; // 从定义的outer标签处(最外层循环处) 进行下一次循环,继续outer处定义的循环
// break outer 跳出outer标签处定义的(最外层循环处) 循环,跳出outer处定义的循环
}
}
System.out.print(i+ " ");
}
}
03章 方法
何谓方法?
**设计方法的原则: **
方法的本意是功能块,就是实现某个功能的语句块的集合。我们设计方法的时候,最好保持方法的原子性,就是一个方法只完成1个功能,这样利于我们后期的扩展。
方法的优点:
使程序变得更简短而清晰。
有利于程序维护。
可以提高程序开发的效率。
提高了代码的重用性。
方法的定义
// 方法名:是方法的实际名称。方法名和参数表共同构成方法签名
修饰符 返回值类型 方法名(参数类型 参数名){
...
方法体
...
return 返回值;
}
方法调用
Java 支持两种调用方法的方式,根据方法是否返回值来选择
当方法有返回值的时候,方法调用通常被当做一个值
int larger = max(30, 40);
如果方法返回值是void,方法调用一定是一条语句
System.out.println("Hello,kuangshen!");
public static void main(String[] args) {
int i = 5;
int j = 2;
int k = max(i, j);
System.out.println( i + " 和 " + j + " 比较,最大值是:" + k);
}
main 方法是被 JVM 调用的,除此之外,main 方法和其它方法没什么区别。JAVA中只有值传递!
main 方法的头部是不变的,带修饰符 public 和 static,返回 void 类型值,方法名字是main,此外带个一个String[] 类型参数。String[] 表明参数是字符串数组。
方法的重载
public static double max(double num1, double num2) {
if (num1 > num2)
return num1;
else
return num2;
}
public static int max(int num1, int num2) {
int result;
if (num1 > num2)
result = num1;
else
result = num2;
return result;
}
拓展命令行传参
命令行参数是在执行程序时候紧跟在程序名字后面的信息。
下面的程序打印所有的命令行参数:
package com.jjh;
public class CommandLine {
public static void main(String args[]){
for(int i=0; i<args.length; i++){
System.out.println("args[" + i + "]: " + args[i]);
}
}
}
命令行:
命令:
$ javac CommandLine.java
$ java com.jjh.CommandLine this is a command line 200 -100
结果:
args[0]: this
args[1]: is
args[2]: a
args[3]: command
args[4]: line
args[5]: 200
args[6]: -100
错误: 找不到或无法加载主类,解决方法:
package com.jjh; // 此处定义了包,所以命令行带上包的路径才能运行成功
public class CommandLine {
public static void main(String[] args) {
for(int i=0; i<args.length; i++){
System.out.println("args[" + i + "]: " + args[i]);
}
}
}
在项目输出的项目目录下(src目录)执行java命令,写完整路径即可:
$ java com.jjh.CommandLine jin jia huan
可变参数
JDK 1.5 开始,Java支持传递同类型 的可变参数 给一个方法。
方法的可变参数的声明如下所示:
typeName... parameterName
一个方法中只能指定一个可变参数,它必须是方法的最后一个参数。任何普通的参数必须在它之前声明。
public static void main(String args[]) {
// 调用可变参数的方法
printMax(34, 3, 3, 2, 56.5);
printMax(new double[]{1, 2, 3});
}
// numbers实际上就是double类型的数组
public static void printMax(double... numbers) {
if (numbers.length == 0) {
System.out.println("No argument passed");
return;
}
double result = numbers[0];
//排序!
for (int i = 1; i < numbers.length; i++) {
if (numbers[i] > result) {
result = numbers[i];
}
}
System.out.println("The max value is " + result);
}
递归
递归就是:A方法调用A方法!就是自己调用自己,因此我们在设计递归算法时,一定要指明什么时候自己不调用自己。否则,就是个死循环!
递归结构包括两个部分
递归头。解答:什么时候不调用自身方法。如果没有头,将陷入死循环
递归体。解答:什么时候需要调用自身方法。
【演示:利用代码计算5的乘阶!】
//5*4*3*2*1
public static void main(String[] args) {
System.out.println(f(5));
}
public static int f(int n) {
if (1 == n){
return 1;
} else {
return n*f(n-1);
}
}
此题中,按照递归的三个条件来分析:
边界条件:阶乘,乘到最后一个数,即1的时候,返回1,程序执行到底
递归前进段:当前的参数不等于1的时候,继续调用自身;
递归返回段:从最大的数开始乘,如果当前参数是5,那么就是5 4,即5 (5-1),即n * (n-1)
能不用递归就不用递归,递归都可以用迭代来代替。
04章 数组
概述
数组的定义:
数组是相同类型 数据的有序集合.
数组描述的是相同类型的若干个数据,按照一定的先后次序排列 组合而成。
其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问 它们
数组声明创建
声明数组
dataType[] arrayRefVar; // 首选的方法
或
dataType arrayRefVar[]; // 效果相同,但不是首选方法
创建数组
arrayRefVar = new dataType[arraySize];
上面的语法语句做了两件事:
使用 dataType[arraySize] 创建了一个数组
把新创建的数组的引用赋值给变量 arrayRefVar
数组变量的声明,和创建数组可以用一条语句完成,如下所示:
dataType[] arrayRefVar = new dataType[arraySize];
获取数组长度:
arrays.length
内存分析
声明的时候并没有实例化任何对象,只有在实例化数组对象时,JVM才分配空间,这时才与长度有关
声明一个数组的时候并没有数组被真正的创建
构造一个数组,必须指定长度
Java内存分析 :
三种初始化
静态初始化
除了用new关键字来产生数组以外,还可以直接在定义数组的同时就为数组元素分配空间并赋值
int[] a = {1,2,3};
Man[] mans = {new Man(1,1),new Man(2,2)};
动态初始化
数组定义、为数组元素分配空间、赋值的操作、分开进行。
int[] a = new int[2];
a[0]=1;
a[1]=2;
数组的默认初始化
数组是引用类型,它的元素相当于类的实例变量,因此数组一经分配空间,其中的每个元素也被按照实例变量同样的方式被隐式初始化。(值就是数据类型的默认值)
public static void main(String[] args) {
int[] a=new int[2];
boolean[] b = new boolean[2];
String[] s = new String[2];
System.out.println(a[0]+":"+a[1]); //0,0
System.out.println(b[0]+":"+b[1]); //false,false
System.out.println(s[0]+":"+s[1]); //null, null
}
基本特点和边界
基本特点
其长度是确定的。数组一旦被创建,它的大小就是不可以改变的
其元素必须是相同类型,不允许出现混合类型
数组中的元素可以是任何数据类型,包括基本类型和引用类型
数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。数组本身就是对象,Java中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型,数组对象本身是在堆中的 。
边界
下标的合法区间:[0, length-1],如果越界就会报错;
ArrayIndexOutOfBoundsException : 数组下标越界异常
数组使用
普通for循环
For-Each 循环
JDK 1.5 引进了一种新的循环类型,被称为 For-Each 循环或者加强型循环,它能在不使用下标的情况下遍历数组。
【示例】
public static void main(String[] args) {
double[] myList = {1.9, 2.9, 3.4, 3.5};
// 打印所有数组元素
for (double element: myList) {
System.out.println(element);
}
}
数组作方法入参
数组可以作为参数传递给方法
比如main方法
数组作返回值
public static int[] reverse(int[] list) {
int[] result = new int[list.length];
for (int i = 0, j = result.length - 1; i < list.length; i++, j--) {
result[j] = list[i];
}
return result;
}
多维数组
多维数组可以看成是数组的数组,比如二维数组就是一个特殊的一维数组,其每一个元素都是一个一维数组
多维数组的动态初始化(以二维数组为例)
直接为每一维分配空间,格式如下:
type[][] typeName = new type[typeLength1][typeLength2];
type 可以为基本数据类型和复合数据类型
arraylenght1 和 arraylenght2 必须为正整数
多维数组的引用(以二维数组为例)
对二维数组中的每个元素,引用方式为 arrayName[index1] [index2] 例如:num[1] [0];
获取数组长度:
a.length获取的二维数组第一维数组的长度,a[0].length才是获取第二维第一个数组长度。
Arrays 类
数组的工具类java.util.Arrays
具有以下常用功能:
给数组赋值:通过 fill 方法
对数组排序:通过 sort 方法,按升序
比较数组:通过 equals 方法比较数组中元素值是否相等。
查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。
数组复制:copyOf(T[] original,int newLength) 从original中复制newLength个元素,返回新数组
常见排序算法
冒泡排序
冒泡排序算法的原理:
比较相邻的元素。如果第一个比第二个大,就交换他们两个
对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数
针对所有的元素重复以上的步骤,除了最后一个
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
public static void main(String[] args) {
// 冒泡排序(倒叙)
int[] a = {2,4,8,1,4,2,7,1,3,5};
// 外层循环,定义一共走几次,每次都能排出最小的数
for (int i = 0; i < a.length-1; i++) {
boolean flag = false; //如果已经有序了,就退出循环,此时已经排列完毕
// 每次内层循环完毕,都能排出最小的数,所以每次内层循环完毕,就能少走一次循环
for (int j = 0; j < a.length-i-1; j++) {
if(a[j] < a[j+1]){
// 换位置
int b=a[j];
a[j]=a[j+1];
a[j+1]=b;
flag=true;
}
}
if(flag==false){
break; // 排序完毕,退出循环
}
}
选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最
大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到排序序列的
末尾。以此类推,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法
package com.jjh.array;
public class SelectSort {
public int[] sort(int arr[]) {
int temp = 0;
for (int i = 0; i < arr.length - 1; i++) {
// 认为目前的数就是最小的, 记录最小数的下标
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) {
// 修改最小值的下标
minIndex = j;
}
}
// 当退出for就找到这次的最小值,就需要交换位置了
if (i != minIndex) {
//交换当前值和找到的最小值的位置
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return arr;
}
public static void main(String[] args) {
SelectSort selectSort = new SelectSort();
int[] array = {2, 5, 1, 6, 4, 9, 8, 5, 3, 1, 2, 0};
int[] sort = selectSort.sort(array);
for (int num : sort) {
System.out.print(num + "\t");
}
}
}
稀疏数组
https://blog.csdn.net/baolingye/article/details/99943083
05章 面向对象
面向对象编程(Object-OrientedProgramming, OOP)
面向对象编程的本质就是:以类的方式组织代码,以对象的方式组织(封装)数据。
初始化和创建对象
对象所占的内存大小
该对象的所有属性所占的内存大小的总和,加上而外的对象隐性数据所占的大小。引用类型变量在32位系统上占4个字节,在64位 系统上占8个字节。
new关键字:
分配内存空间
给对象进行默认的初始化(属性的初始化,基本数据类型默认值是定义的默认值,引用数据类型默认值是null)
调用类中的构造方法
构造器
定义有参构造器后,要显示定义无参构造器,否则无参构造器就会不存在
内存分析
this关键字
代码块
普通代码块:方法中的代码块
构造块:类中定义的代码块
在每一次创建对象时执行(并不是在类加载时执行),始终在构造方法前执行。
静态代码块:类中使用static修饰的代码块,在类加载的时候执行,并且只会执行一次
同步代码块(多线程中讲解)
访问修饰符
同类 同包 不同包子类 其他
public √ √ √ √
protected √ √ √
default √ √
private √
创建对象
创建对象会调用构造方法,调用构造方法不一定就是创建对象(子类构造方法第一行会调用父类的构造方法,但不会创建父类的对象)
封装
白话:该露的露,该藏的藏
专业:我们程序设计要追求**“高内聚,低耦合” **。
高内聚:就是类的内部数据操作细节自己完成,不允许外部干涉;
低耦合:仅暴露少量的方法给外部使用
封装(数据的隐藏)
记住这句话就够了:属性私有,set/get
作用和意义
提高程序的安全性,保护数据。
隐藏代码的实现细节
统一用户的调用接口
提高系统的可维护性
便于调用者调用。
继承
继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模
extands的意思是“扩展”。子类是父类的扩展
继承
继承是类和类之间的一种关系。除此之外,类和类之间的关系还有依赖、组合、聚合等
继承关系的俩个类,一个为子类(派生类),一个为父类(基类)。子类继承父类,使用关键字extends来表示
子类和父类之间,从意义上讲应该具有"is a"的关系
student is a person
dog is a animal
类和类之间的继承是单继承(接口和接口之间是多继承)
父类中的属性和方法可以被子类继承(可以理解为私有的属性和方法无法被继承,和下面讲述的有偏差,但效果一样)
子类中继承了父类中的属性和方法后,在子类中能不能直接使用这些属性和方法,是和这些属性和方法原有
的修饰符(public protected default private)相关的。
父类中的属性和方法使用public修饰,在子类中继承后"可以直接"使用
父类中的属性和方法使用private修饰,在子类中继承后"不可以直接"使用
父类中的构造器是不能被子类继承的,但是子类的构造器中,会隐式的调用父类中的无参构造器(默认使用super)
super关键字
super调用父类的属性、方法、构造方法,但是private修饰的不能被调用
super调用父类构造方法,必须是子类构造方法中的第一个语句。
super只能出现在子类的方法或者构造方法中。
super 和 this 不能够同时调用构造方法。(因为this也是在构造方法的第一个语句)
super 和 this 的区别
方法重写
重写只跟非静态方法有关
静态方法:
静态属性和静态方法的调用只和变量声明的类型相关
举例:
public class Demo01 {
public static void main(String[] args) {
A a = new A();
B b = new A(); // 向上类型转换
System.out.println(a.name); // A
System.out.println(b.name); // B
a.test(); // A→test()
b.test(); // B→test()
}
}
class A extends B {
public static String name="A";
public static void test(){
System.out.println("A→test()");
}
}
class B{
public static String name="B";
public static void test(){
System.out.println("B→test()");
}
}
非静态方法
两个类有继承关系,子类重写父类的方法
方法名必须相同,参数列表必须相同(方法签名相同),方法体不同
修饰符:范围可以扩大但不能缩小(子类修饰符>=父类修饰符) public>protected>default>private
抛出的异常范围:可以缩小但不能扩大(子类异常<=父类异常)
父类中的方法若使用private、static、final任意修饰符修饰,不能被子类重写
static 静态方法,属于类,不属于实例
final 常量
private 私有的方法
多态
认识多态
动态编译:类型 可扩展性
理解:
Student s1 = new Student(); // 学生的对象可以是个学生
Person s2 = new Student(); // 学生也是个人
Object s3 = new Student(); // 学生也是个Object
// 一个Student对象,拥有Student、Person、Object三种形态
子类能调用自己的方法和继承父类的方法
父类可以指向子类,但是不能调用子类独有的方法
多态注意事项
多态是多态的方法,属性 没有多态
父类和子类有联系 否则会有类型转换异常
存在条件:继承关系,方法需要重写,父类的引用指向子类的对象
多态存在的条件
有继承关系
子类重写父类方法
以下三种类型的方法是没有办法表现出多态特性的
static方法,因为被static修饰的方法是属于类的,而不是属于实例
final方法,因为被final修饰的方法无法被子类重写
private方法,因为被private修饰的方法对子类不可见
父类引用指向子类对象
instanceof和类型转换
instanceof
public class Person{
public void run(){}
}
public class Student extends Person{
}
public class Teacher extends Person{
}
main:
Object object = new Student();
// 解释:当前的这个object对象引用指向的是Student对象,对应Student→Person→Object线路,所以前三个为true
System.out.println(object instanceof Student); //true
System.out.println(object instanceof Person); //true
System.out.println(object instanceof Object); //true
System.out.println(object instanceof Teacher); //false
System.out.println(object instanceof String); //false
Person object = new Student();
System.out.println(object instanceof Student); //true
System.out.println(object instanceof Person); //true
System.out.println(object instanceof Object); //true
System.out.println(object instanceof Teacher); //false
System.out.println(object instanceof String); // 编译报错
Student object = new Student();
System.out.println(object instanceof Student); //true
System.out.println(object instanceof Person); //true
System.out.println(object instanceof Object); //true
System.out.println(object instanceof Teacher); //编译报错
System.out.println(object instanceof String); // 编译报错
类型转换
自动类型转换(低→高) 可能会丢失自己本来的一些方法
强制类型转换(高→低)
修饰符
static关键字
属于类,不属于实例对象,随着类的加载而出现,类的卸载而消失
static属性
static方法
可通过 类名.方法名 的方式调用
能调用静态属性,静态方法。不能调用非静态属性、非静态方法
static方法出现的比非静态属性、非静态方法早,调用根本找不到
this和super在类中属于非静态的变量,不能在静态方法中使用
static代码块
随着类加载被掉用,只调用一次
执行顺序:静态代码块>构造块(匿名代码块)>构造器(构造方法)
静态导入
静态导包就是java包的静态导入,用import static代替import ,静态导入包是JDK1.5中的新特性
意思是导入这个类里的静态方法
import static java.lang.Math.random;
import static java.lang.Math.PI;
public class Test {
public static void main(String[] args) {
//之前是需要Math.random()调用的
System.out.println(random());
System.out.println(PI);
}
}
或者
import static java.lang.System.*;
import static java.lang.Math.*;
public class Demo02 {
public static void main(String[] args) {
//之前是需要Math.random()调用的
out.println(random());
out.println(PI);
}
final关键字
声明常量:修饰属性或者修饰局部变量(最终变量),不能被修改
修饰方法:只能被子类继承,但是不能被子类重写
修饰类:fianl修饰的类无法被继承
方法参数中使用final,在该方法内部不能修改参数的值(在内部类中详解)
常量类
1.在该类中只有常量,通常是应用程序中公共的常量或标记
抽象类
可以没有抽象方法,有抽象方法的类必须是抽象类
不能创建对象,只能被继承,不能用final修饰
非抽象类继承抽象类必须实现所有抽象方法
抽象类可以继承抽象类,可以不实现父类抽象方法。
抽象类可以有构造方法
接口
约束,定义一些方法,不同的实现类实现
接口中都是抽象方法和常量,常量默认被public static final,方法默认被public abstract修饰,
java1.8出现static方法:static方法正常使用,不影响使用契约。
java1.8出现default方法实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
接口可以继承多个接口,不实现接口的方法
一个类可以实现多个接口,抽象类实现接口可以不实现接口的方法
总结:
类的关系:单继承
接口的关系:多继承
类和接口的关系:多实现
内部类(小点代码)
内部类就是在一个类的内部在定义一个类,比如,A类中定义一个B类,那么B类相对A类来说就称为内部类,而A类相对B类来说就是外部类了。
成员内部类
package com.innerclass;
public class Outer {
private String name="zhangsan";
private int age=23;
class Inner{
private String address="hebei";
private String phone="835473950874350";
public void show(){
// 访问外部类的属性
System.out.println(name);
System.out.println(age);
// 访问本类的属性
System.out.println(address);
System.out.println(phone);
}
}
public static void main(String[] args) {
Outer outer = new Outer();
Inner inner = outer.new Inner();
inner.show();
}
}
在类的内部定义,与实例变量、实例方法同级别的类
成员内部类中不能写静态属性和方法,但是能包含静态常量(不能包含static final 修饰的方法 )
private static final String HOBBY="篮球";
外部类的一个实例部分,创建内部类对象时,必须依赖外部类对象
Outer outer = new Outer();
Inner inner = outer.new Inner();
当外部类和内部类存在重名属性时,会优先访问内部类的属性
静态内部类:
package com.innerclass;
public class StaticInnerTest{
public static void main(String[] args) {
StaticOuter.Inner inner = new StaticOuter.Inner();
inner.show();
}
}
class StaticOuter {
private String name="xxx";
private int age=18;
// 静态内部类,和外部类相同(和外部类在同一级别)
static class Inner{
private String address="shanghai";
private String phone="930547215";
// 静态变量
private static int count=10;
public void show(){
// 调用外部类属性
StaticOuter outer = new StaticOuter();
System.out.println(outer.name);
System.out.println(outer.age);
// 调用本类的属性和方法
System.out.println(address);
System.out.println(phone);
System.out.println(Inner.count);
}
}
}
static修饰的内部类就叫静态内部类。
和外部类在同一个级别,所以访问外部类实例成员,需要创建外部类对象
不依赖外部类对象,可直接创建或者通过类名访问,可声明静态成员
StaticOuter.Inner inner = new StaticOuter.Inner();
inner.show();
只能直接访问外部类的静态成员,访问外部类的实例成员需要实例化外部类对象
StaticOuter outer = new StaticOuter();
outer.name
局部内部类:
定义在外部类的方法当中,作用范围和创建对象范围仅限于当前方法,所以
package com.innerclass;
public class JuBuInnerTest {
public static void main(String[] args) {
JuBuOuter juBuOuter = new JuBuOuter();
juBuOuter.show();
}
}
class JuBuOuter{
private String name="zhangsan";
private int age=12;
// 外部方法
public void show(){
// 定义局部变量
String address="shanghai";
// 局部内部类:不能加任何的访问修饰符
class Inner{
// 局部内部类的属性
private String phone="9543793075";
private String email="3842@qq.com";
// 定义静态常量
static final int count=2000;
// 局部内部类的方法
public void show2(){
// 访问外部类的属性
System.out.println(JuBuOuter.this.name);
System.out.println(JuBuOuter.this.age);
System.out.println(phone);
System.out.println(email);
// 访问局部变量,jdk1.7要求,变量必须是常量final,jdk1.8自动添加final
System.out.println(address);
}
}
Inner inner = new Inner();
inner.show2();
}
}
局部内部类和成员内部类是一样的(访问外部类的属性和方法,重名机制,不能定义静态变量(但是可以定义静态常量)、方法等,要注意以下四点区别:
局部内部类不能有任何的访问修饰符
当外部类的方法是静态方法时,局部内部类不能访问外部类的非静态属性和方法
外部类方法定义的局部变量就是常量
原因:根据上面的例子解释
如果不是常量,局部变量address就会随着show方法执行完毕而消失
但是局部内部类Inner所在的方法区数据和Inner对象inner指向的堆内存数据不会立刻消失
show2方法此时会找不到局部变量address,所以要定义为常量,放在常量池中
局部内部类要在外部类方法中创建对象
匿名内部类
没有名字的局部内部类(一切特征都与局部内部类相同)
package com.innerclass;
public class NiMingInnerTest {
public static void main(String[] args) {
// 局部内部类
class Inner implements USB {
@Override
public void service() {
System.out.println("风扇接口,风扇转起来了");
}
}
// 调用局部内部类实现接口的方法
new Inner().service();
// 匿名内部类,可以把{...}看成接口的实现类,只是没有名字。new 实现类 就得到了对象
USB usb=new USB(){
@Override
public void service() {
System.out.println("键盘接口,键盘开始工作了");
}
};
// 调用匿名内部类实现接口的方法
usb.service();
}
}
// 定义接口
interface USB {
// 接口方法的默认修饰符 public abstract
void service();
}
必须继承一个父类或者实现一个接口
定义类,实现类,创建对象的语法合并,只能创建一个该类的对象
优点:减少代码量
缺点:可读性较差
难点理解
静态变量和静态常量
为什么成员/局部内部类不能有静态变量,但是可以有静态常量
为什么不能有静态变量
静态变量是要占用内存的,在编译时只要是定义为静态变量了,系统就会自动分配内存给他。
因为系统执行:运行宿主类->静态变量内存分配->内部类
java虚拟机要求所有的静态变量在类加载过程中的初始化阶段 将符号引用变为直接引用,来为其分配内存。
而static的语义,就是主类能直接通过内部类名来访问内部类中的static方法,而非static的内部类又是不会自动加载的,就会造成冲突
为什么可以有静态常量
首先理解编译期常量和非编译期常量
编译期常量:static final int num=20
非编译期常量(运行期常量):static final int num=(int) Math.random()
编译期常量是不需要加载类的字节码文件的,会在编译阶段确定值,很多书上将这一步称之为编译期常量折叠【编译器在编译阶段通过语法分析计算出常量表达式的具体值】。
非编译期常量,对于这样的值而言编译期无法折叠,编译器只能做一些语法检查,比如该常量之是否在其他地方做了修改等 。既然无法确定值,那么就会导致该常量值的确定需要类被加载时确定,所以就和静态常量是一样的。
数据是否能够访问的依据
内部类种类有多种,内部类和外部类的数据相互访问成为一个十分难理解的点。比如每一种内部类,外部类能够访问内部类的东西有什么。内部类能够访问外部类的东西有什么。我们该依据什么进行判断????
判断是否能够访问的依据就是:在用到这个东西的时候,分析这个东西存在还是不存在 ,所以前提是要了解内部类加载的时机。这里的东西 指的这个类或属性或方法
举例说明:
成员内部类
外部类访问内部类的东西
外部类的静态部分能不能访问内部类的东西,
分析:当在静态部分访问内部类某个东西的时候,这个东西是不存在的,因为在访问的时候,不会引起内部类的加载,这个类都没有加载进来,肯定是没有这个东西的 。
外部类的实例部分能不能访问内部类的东西
分析:外部类的实例部分依赖于外部类的对象,当访问这个内部类时,这个内部类是存在的,因为会引起内部类的加载,内部类加载进来之后,就可以通过这个内部类去创建对象,调用里面的东西。当时如果直接访问内部类里面的东西,这个东西是不存在的 ,因为访问里面的东西,并不会引起内部类的加载,所以东西不存在。
内部类访问外部类的东西
因为内部类在使用外部类东西的时候,外部类已经被加载进来,并且还创建了外部类的对象。所以不管是外部类的静态的东西还是实例的东西,内部类都能够进行直接的访问。因为这些东西统统存在。
静态内部类
外部类访问内部类的东西
外部类的静态部分能不能访问内部类的东西
分析:外部类的静态部分如果使用到这个类,会引起内部类的加载。因为加载所以内部类会存在,外部类能使用内部类。能不能直接使用内部类静态部分和实例部分,不能,因为使用这个东西的时候,不会导致内部类加载,所以不存在。
外部类的实例部分能不能访问内部类的东西
分析:实例部分可以使用内部类,因为会导致内部类的加载,所以内部类会存在。但是不能使用内部类里面的东西,因为直接使用不会导致内部类的加载,所以这些东西不存在
内部类访问外部类的东西
内部类可以使用外部类,因为在使用的时候,外部类肯定已经加载了。因为内部类在加载之前,如果外部类没有加载,会先加载外部类,在加载内部类。所以外部类在使用的时候一定存在。内部类用外部类的静态部分,因为外部类的静态东西已经存在,能直接用。但是内部类不能直接用外部类的实例部分,因为虽然外部类已经加载进来,但是并没有创建外部类的对象,针对内部类来说,外部类的对象还不存在,不能使用实例部分。
06章 异常
概念
检查性异常
最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的,例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略
运行时异常
运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略
错误
错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的
异常体系结构
Java把异常当作对象来处理,并定义一个基类 java.lang.Throwable 作为所有异常的超类
异常之间的区别与联系
Error
Error 类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关
比如:
Java虚拟机运行错误( Virtual MachineError )
发生在虚拟机试图执行应用时如类定义错误( NoClassDefFoundError )、链接错误( LinkageError )
对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况
Exception
在 Exception 分支中有一个重要的子类 RuntimeException (运行时异常)
ArrayIndexOutOfBoundsException (数组下标越界)
NullPointerException (空指针异常)
ArithmeticException (算术异常)
MissingResourceException (丢失资源)
ClassNotFoundException (找不到类)
这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生
而RuntimeException 之外的异常我们统称为非运行时异常,类型上属于 Exception 类及其子类从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。
IOException
SQLException
用户自定义的 Exception 异常
Error 和 Exception 的区别:
Error 通常是灾难性的致命错误,程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程
Exception 通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常
异常处理的语法规则
try、catch、finally语句不能单独存在,try...catch...finally、try...catch、try...finally三种结构。catch语句可以有一个或多个,finally语句最多一个。
try、catch、finally三个代码块中变量的作用域分别独立而不能相互访问。
多个catch块时候,从小到大的范围去捕获异常,Java虚拟机会匹配其中一个异常类或其子类,就执行这个catch块,而不会再执行别的catch块
异常中return处理机制
try和catch中如果有return,先将返回值保存到栈内存中。执行finally语句完毕,在执行try或者catch中的return语句,将返回值返回。
执行try,catch , 给返回值赋值
执行finally
return
throw和thows
throw:常用在方法中手动抛出异常
throws:在方法上抛出异常
经验总结
assert关键字,表示断言。
断言语句一般用于程序不准备通过抛出异常来处理的错误。
assert expression; expression为true,往下执行。为false,出现异常,程序退出。
assert expression1:expression2; expression1为true,忽略expression2,往下执行。为false,运行expression2,然后退出程序。expression2是一个基本类型或者Object类型
开启断言
断言的格式
// 布尔表达式为真,则继续执行,否则,程序停止执行。
assert 布尔表达式 :“错误说明”(错误说明可有可无,不影响)
举例
// 如果条件为true,程序继续执行
public static void main(String[] args) {
int i =10;
assert i>0:"i必须是大于0的数字";
System.out.println(i);
}
打印:
10
=========================================================================================
// 如果添加为假,抛出错误,程序结束执行
public static void main(String[] args) {
int i =-10;
assert i>0:"i必须是大于0的数字";
System.out.println(i);
}
打印:
Exception in thread "main" java.lang.AssertionError: i必须是大于0的数字
at com.mycode.jichu.Test06.main(Test06.java:7)
断言的优点
使用Assert可以避免很多的if else嵌套,比起抛出异常的方式,Aseert能更简洁。
Assert可以减少代码单侧case(案例),从而减少代码单侧代码量,还能提高代码单侧覆盖率
断言能够帮助别人或未来的你理解代码,找出程序中逻辑不对的地方。一方面,断言会提醒你某个对象应该处于何种状态
07章 常用类库
Object
超类、基类,所有类都直接或者间接的继承此类(包括数组),位于继承树的最顶层
任何类,如果没有显示的extends继承某个类,默认继承的就是Object类,否则为间接继承Object类定义的方法,是所有对象都具备的方法
Object类型可以存储任何对象
作为参数,可接受任何对象
作为返回值,可返回任何对象
getClass
获取运行时对象所属类的类对象
public final native Class<?> getClass();
hashCode
返回该对象的哈希码值
哈希值根据对象的引用地址或字符串或数字使用hash算法计算出来的int类型的数值
一般情况下,相同对象返回相同的哈希码值
public native int hashCode();
toString
一般情况下,子类都重写此方法
public String toString() {
// getClass().getName() 类名
// @ 标记位
// Integer.toHexString(hashCode()) 此对象哈希码的无符号十六进制表示组成
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
equals
public boolean equals(Object obj) {
return (this == obj);
}
重写equals方法步骤:
class Student{
private String name;
private String age;
public Student() {
}
public Student(String name, String age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
// 判断两个对象是否是同一个引用
if(super.equals(obj)){
return true;
}
// 判断obj是否是null
if(obj==null){
return false;
}
// 判断是否是同一个类型
if(obj instanceof Student){
// 进行强制类型转换
Student s=(Student)obj;
// 比较属性
if(this.name.equals(s.name) && this.age==s.age){
return true;
}
}
return false;
}
}
判断引用地址
判断传入的对象是否为null
判断是否属于同一个类型
进行强制类型
比较属性是否相等
finalize
protected void finalize() throws Throwable { }
当对象被判定为垃圾对象时,有jvm自动调用此方法,用以标记垃圾对象,进入回收队列。这样的话,jvm在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
垃圾对象:没有任何引用的对象
System.gc(),通知垃圾回收器该回收垃圾了,垃圾回收器会将对象放进垃圾队列,垃圾回收器到底回不回收,由自身决定
class Student{
private String name;
private String age;
public Student() {
}
public Student(String name, String age) {
this.name = name;
this.age = age;
}
@Override
protected void finalize() throws Throwable {
System.out.println(this.name+"回收了");
}
public static void main(String[] args) {
// 创建垃圾对象,因为在栈内存中没有任何的引用
new Student("张三","17");
new Student("李四","18");
new Student("王五","18");
new Student("赵六","18");
// 通知垃圾回收器回收垃圾对象,只是通知
// 垃圾回收器发现有垃圾对象,就会将对象放入回收队列,等待下次发起回收动作进行回收
System.gc();
}
}
包装类
Java为每种基本数据类型分别设计了对应的类,称之为包装类(Wrapper Classes),也有教材称为外覆类或数据类型类
每个包装类的对象可以封装一个相应的基本类型的数据,并提供了其它一些有用的方法。包装类对象一经创建,其内容(所封装的基本类型数据值)不可改变。
以Integer为例子
private final int value;
装箱和拆箱
基本类型和对应的包装类可以相互装换:
由基本类型向对应的包装类转换称为装箱,例如把 int 包装成 Integer 类的对象
包装类向对应的基本类型转换称为拆箱,例如把 Integer 类的对象重新简化为 int
以Integer为例:
// jdk1.5之前
// 装箱
Integer integer1=new Integer(10);
Integer integer2 = Integer.valueOf(10);
// 拆箱
int i1 = integer1.intValue();
// jdk1.5之后,自动装箱和拆箱
// 自动装箱
Integer integer3=10;
// 自动拆箱
int i3=integer3;
类型转换
类型转换需要保证类型兼容,否则就会出现类型转换异常
8种包装类提供不同类型间的转换方式
Number父类中提供的6个共性方法
parseXXX() 静态方法 字符串转换成基本类型
valueOf() 静态方法 基本类型转换为包装类型
// 基本类型和字符串之间的转换
// 基本类型转换成字符串
System.out.println(10 + "");
System.out.println(Integer.toString(10));
System.out.println(Integer.toString(15, 16)); // 指定进制 16表示16进制
// 字符串转换为基本类型
int i1 = Integer.parseInt("150");
// boolean字符串转成基本类型 "true"→true,非"true"→false
System.out.println(Boolean.parseBoolean("true")); // true
System.out.println(Boolean.parseBoolean("false")); // false
System.out.println(Boolean.parseBoolean("dkjflasdjfs")); // flase
Integer缓冲区
java预先创建256个常用的整数包装类型的对象
取值范围:【-128 - 127】
// 示例1
Integer integer1 = new Integer(10);
Integer integer2 = new Integer(10);
System.out.println(integer1 == integer2); // false
// 示例2
Integer integer3 = Integer.valueOf(100);
Integer integer4 = Integer.valueOf(100);
System.out.println(integer3 == integer4); // true
// 示例3
Integer integer5 = 100;
Integer integer6= 100;
System.out.println(integer5 == integer6); // true
// 示例3
Integer integer7 = Integer.valueOf(200);
Integer integer8 = Integer.valueOf(200);
System.out.println(integer7 == integer8); // false
String
字符串是常量,创建之后不可改变
字符串字面值存储在字符串常量池中(常量池在方法区中),目的就是为了可以实现共享
创建和内存分析
String s = "lisi";产生一个对象,字符串常量池存储"lisi"
String s1="lisi";
System.out.println(s1.hashCode()); // 3322003
// 字符串不可变性指的是在字符串常量池中并没有把"lisi"改为"zhangsan",而是重新开辟了内存空间,新增了"zhangsan"
// 然后把变量s1的16进制引用地址改成了引用"zhangsan"的16进制地址
s1="zhangsan";
System.out.println(s1.hashCode()); // -1432604556
String s2="zhangsan";
System.out.println(s1.hashCode()); // -1432604556
System.out.println(s2.hashCode()); // -1432604556
System.out.println(s1 == s2); // true
String s1 = new String("lisi");产生两个对象,堆、池各存储一个
String s1 = new String("lisi");
String s2 = new String("lisi");
System.out.println(s1 == s2); // false
常用方法
compareTo
比较两个字符串在字典中的位置(大小)
String s1 = "abc"; // 【a:97】【b:98】【c:99】
String s2 = "xyzwerwe"; // 【x:120】【y:121】【z:122】
// 比较原则1
// s1的a先和s2的x通过字符编码集对应的数字值进行比较(减操作),
// 如果为0(代表两个字符相等),比较下一个字符,否则返回相减后的值
System.out.println(s1.compareTo(s2)); // -23
String s3 = "abc";
String s4 = "abcwerwe";
// 比较原则2
// 如果通过比较原则1得不到结果,就比较两个字符串的长度,返回长度相减后的值
System.out.println(s3.compareTo(s4)); // -5
StringBuffer和StringBuilder
StringBuffer:
可变长字符串,jdk1.0提供,运行效率缓慢、线程安全
StringBuilder:
可变长字符串,jdk5.0提供,运行效率快、线程不安全
以下方法通用,以StringBuffer为例:
StringBuffer stringBuffer = new StringBuffer();
// append 追加
stringBuffer.append("zhangsan "); // zhangsan
stringBuffer.append("lisi "); // zhangsan lisi
// insert 添加
stringBuffer.insert(0,"wangwu "); // wangwu zhangsan lisi
// replace 指定索引区间(左闭右开)替换,String只能替换指定的旧字符串
stringBuffer.replace(0,7,"zhaoliu "); // zhaoliu zhangsan lisi
// delete 指定索引区间(左闭右开)删除
stringBuffer.delete(0,8); // zhangsan lisi
// 清空
stringBuffer.delete(0,stringBuffer.length()-1); // 啥也没有,空字符串
BigDecimal
由于float和double使用的是近似存储,会丢失精度(无限的接近于准确值),所以使用BigDecimal,能精确计算浮点数
double d1 = 0.3;
double d2 = 0.2;
System.out.println(d1-d2); // 0.09999999999999998
System.out.println((d1-d2)/d2); // 0.4999999999999999
BigDecimal d3 = new BigDecimal("0.3");
BigDecimal d4 = new BigDecimal("0.2");
// 减
BigDecimal subtract3 = d3.subtract(d4); // 0.1
// 加
BigDecimal add3 = d3.add(d4); // 0.5
// 乘
BigDecimal multiply3 = d3.multiply(d4); // 0.06
// 除
BigDecimal divide3 = d3.divide(d4); // 1.5
// 使用除法如果除不尽,就会产生异常,通过保留小数的形式解决
BigDecimal bigDecimal1 = new BigDecimal("10.0");
BigDecimal bigDecimal2 = new BigDecimal("3.0");
// 2 保留小数的位数 BigDecimal.ROUND_HALF_UP 采用四舍五入的方式
BigDecimal divide1 = bigDecimal1.divide(bigDecimal2,2, BigDecimal.ROUND_HALF_UP); // 3.33
Date
表示特定的瞬间,精确到毫秒。
Date类中的大部分方法都已经被Calender类中的方法所取代
时间单位:
1秒=1000毫秒
1毫秒=1000微秒
1微秒=1000纳秒(毫微秒)
// 今天
Date date = new Date();
System.out.println(date); // Wed May 10 23:48:19 CST 2023
System.out.println(date.toLocaleString()); // 2023-5-10 23:48:19
// 昨天
Date date2 = new Date(date.getTime()-24*60*60*1000);
System.out.println(date2.toLocaleString()); // 2023-5-9 23:48:19
// after 是否在data2之后
System.out.println(date.after(date2)); // true
// before 是否在data2之前
System.out.println(date.before(date2)); // false
// compareTo 比较两个日期的毫秒值,减操作,如果>0,返回1,<0 返回-1,=0 返回0
System.out.println(date.compareTo(date2)); // 1
// equals 比较两个时间是否相等
System.out.println(date.equals(date2)); // false
// getTime 从1970.1.1 00:00:00 时刻到date时刻的毫秒数(针对中国时区是1970.1.1 08:00:00)
System.out.println(date.getTime()); // 1683734451144
// setTime 从1970.1.1 00:00:00 时刻开始,设置的毫秒点(针对中国时区是1970.1.1 08:00:00)
date.setTime(24*60*60*1000);
System.out.println(date.toLocaleString()); // 1970-1-2 8:00:00
Calendar
提供了获取和设置各种日历字段的方法
其构造方法使用了protected修饰,因此无法直接创建对象
基本操作
Calendar calendar = Calendar.getInstance();
// getTimeInMillis 获取毫秒值(针对中国时区是1970.1.1 08:00:00)
System.out.println(calendar.getTimeInMillis()); // 1683744245628
// getTime() 转换为Date类型,设置日期(DATE,DAY_OF_MONTH)和小时(HOUR,AM,PM,HOUR_OF_DAY)的默认值
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-11 2:44:05
获取时间字段
Calendar calendar = Calendar.getInstance(); // 2023-5-11 2:44:05
// YEAR 获取年
int year = calendar.get(Calendar.YEAR); // 2023
// MONTH 获取月
int month = calendar.get(Calendar.MONTH); // 4 月份从0开始(0是1月)
// DAY_OF_MONTH 获取日
int day = calendar.get(Calendar.DAY_OF_MONTH); // 11
// HOUR 获取小时 12小时制
int hour12 = calendar.get(Calendar.HOUR); // 2 HOUR_OF_DAY 24小时
// MINUTE 分钟
int minute = calendar.get(Calendar.MINUTE); // 44
// SECOND 秒
int second = calendar.get(Calendar.SECOND); // 5
修改时间
Calendar calendar2 = Calendar.getInstance();
System.out.println(calendar2.getTime().toLocaleString()); // 2023-5-11 0:40:59
calendar2.set(Calendar.MONTH,3); // 设置3,其实设置的是4月
System.out.println(calendar2.getTime().toLocaleString()); // 2023-4-10 0:40:59
calendar2.set(Calendar.DAY_OF_MONTH,10);
System.out.println(calendar2.getTime().toLocaleString()); // 2023-5-10 0:40:59
计算(运算)时间
Calendar calendar3 = Calendar.getInstance();
System.out.println(calendar3.getTime().toLocaleString()); // 2023-5-11 0:49:57
calendar3.add(Calendar.DAY_OF_MONTH,1); // 加1天
System.out.println(calendar3.getTime().toLocaleString()); // 2023-5-12 0:49:57
清空时间
Calendar calendar4 = Calendar.getInstance();
System.out.println(calendar4.getTime().toLocaleString()); // 2023-5-11 2:44:05
// 清空年
calendar4.clear(Calendar.YEAR);
System.out.println(calendar4.getTime().toLocaleString()); // 1970-5-11 2:44:05
// 清空月
calendar4.clear(Calendar.MONTH);
System.out.println(calendar4.getTime().toLocaleString()); // 1970-1-11 2:44:05
// 清空分钟
calendar4.clear(Calendar.MINUTE);
System.out.println(calendar4.getTime().toLocaleString()); // 1970-1-11 2:00:05
// 清空秒
calendar4.clear(Calendar.SECOND);
System.out.println(calendar4.getTime().toLocaleString()); // 1970-1-11 2:00:00
// 全部清空,一步到位
calendar4.clear();
System.out.println(calendar4.getTime().toLocaleString()); // 1970-1-1 0:00:00
// 此时的毫秒数为-28800000而不是0,说明开始计算时间从中国的 1970-1-1 08:00:00 时为开始时间
// 因为和标准地区的时间相差8个小时
System.out.println(calendar4.getTimeInMillis()); // -28800000
清空日期和小时(不生效)
清空时间针对日期(DATE,DAY_OF_MONTH)和小时(HOUR,AM,PM,HOUR_OF_DAY)不会生效
调用getTime()方法会设置其默认值
清空日期演示
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-11 3:04:45
// 清空不生效,此时的默认日期还是11号
calendar.clear(Calendar.DAY_OF_MONTH);
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-11 3:04:45
// 设置日期为22号,但是默认日期仍然是11号
calendar.set(Calendar.DAY_OF_MONTH,22);
// 清空不生效,此时的默认日期还是11号
calendar.clear(Calendar.DAY_OF_MONTH);
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-11 3:04:45
// 设置日期为22号,但是默认日期仍然是11号
calendar.set(Calendar.DAY_OF_MONTH,22);
// getTime() 会将默认日期由11号改为设置的22号
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-22 3:04:45
// 清空不生效,但是默认日期是22号
calendar.clear(Calendar.DAY_OF_MONTH);
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-22 3:04:45
清空小时演示
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-11 3:16:48
// 清空不生效,此时的默认小时还是3时
calendar.clear(Calendar.HOUR_OF_DAY);
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-11 3:16:48
// 设置小时为22时,但是默认小时仍然是3时
calendar.set(Calendar.HOUR_OF_DAY,22);
// 清空不生效,此时的默认小时还是3时
calendar.clear(Calendar.HOUR_OF_DAY);
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-11 3:16:48
// 设置小时为22时,但是默认小时仍然是3时
calendar.set(Calendar.HOUR_OF_DAY,22);
// getTime() 会将默认小时由3时改为设置的22时
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-11 22:16:48
// 清空不生效,但是默认小时是22时
calendar.clear(Calendar.HOUR_OF_DAY);
System.out.println(calendar.getTime().toLocaleString()); // 2023-5-11 22:16:48
补充方法
Calendar calendar5 = Calendar.getInstance();
System.out.println(calendar5.getTime().toLocaleString()); // 2023-5-11 2:44:05
// 获取最大值
int actualMaximum = calendar5.getActualMaximum(Calendar.DAY_OF_MONTH); // 获取本月最大值 31
// 获取最小值
int actualMinimum = calendar5.getActualMinimum(Calendar.DAY_OF_MONTH); // 获取本月最小值 1
进行日期格式化
// 字符串→日期
Date date = format.parse("1970-1-1 00:00:00");
System.out.println(date.toLocaleString()); // 1970-1-1 0:00:00
// 日期→字符串
String formatStr = format.format(date);
System.out.println(formatStr); // 1970-01-01 00:00:00
System
// 数组复制
int[] arr = {23,432,542,31,545,867,13,654};
int[] dest = new int[8];
// arr 源数组
// srcPost 开始位置
// dest 目标数组
// destPost 目标数组开始位置
// length:复制长度
// 效率高
System.arraycopy(arr,0,dest,0,arr.length);
System.out.println(Arrays.toString(dest)); // [23, 432, 542, 31, 545, 867, 13, 654]
// 效率低
int[] copy = Arrays.copyOf(arr, arr.length);
System.out.println(Arrays.toString(copy)); // [23, 432, 542, 31, 545, 867, 13, 654]
// 获取毫秒数,经常用来计算某段逻辑代码的运行时间
long currentTimeMillis = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
System.out.println(i);
}
System.out.println(System.currentTimeMillis()-currentTimeMillis); // 31(毫秒)
// 告知垃圾回收器回收垃圾,会将垃圾对象放进回收队列,当jvm真正发起回收动作时,垃圾对象才被真正的回收
System.gc();
// 手动退出程序
System.exit(0);
表达式
1.语法:(parameters) -> { statements; }(重写抽象方法)
2.举例:TwoParameterNoReturn twoParameterNoReturn = (str1, str2) -> {System.out.println(str1 + str2);};
3.匿名内部类只能使用被 final 修饰的局部变量。Lambda 表达式中使用的局部变量可以不用声明为 final,但是必须不可 被后面的代码修改,即隐性的具有 final 的语义。
4.在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者变量。
08章 泛型
是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型
定义规则
1.泛型类:修饰符 class 类名<代表泛型的变量> { } public class GenericsClassDemo
2.泛型接口:修饰符 interface接口名<代表泛型的变量> { } public interface GenericsInteface
3.泛型方法:修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ } public T genercMethod(T t)
通配符
1.?,可以传入任意的类型,但是只是接收输出,并不能修改
2.泛型的上限,类型名称 <? extends 类 > 对象名称,能接收该类型及其子类。
1.ArrayList<? extends Animal> list = new ArrayList();//报错
2.ArrayList<? extends Animal> list2 = new ArrayList();
3.ArrayList<? extends Animal> list3 = new ArrayList();
3.泛型的下限,类型名称 <? super 类 > 对象名称,只能接收该类型及其父类型
1.ArrayList<? super Animal> list5 = new ArrayList();
2.ArrayList<? super Animal> list6 = new ArrayList();
3.ArrayList<? super Animal> list7 = new ArrayList();//报错
09章 集合
对象的容器,实现了对对象的常用操作,类似数组的功能
集合和数组的区别:
数组长度固定,集合长度不固定
数组可以存储基本数据类型和引用数据类型,集合只能存储引用数据类型
Collection体系集合
特点:代表一组任意类型的对象,无序、无下标、不能重复
常用方法
方法
作用
boolean add(E e)
添加指定元素到集合中
boolean addAll(Collection<? extends E c)
将一个相同类型的集合中所有对象添加到当前集合
void clear()
清空此集合的所有对象
boolean contains(Object o)
检查此集合中是否包含指定的对象
boolean equals(Object o)
比较此集合是否与指定对象相等
boolean isEmpty()
判断此集合是否为空
boolean remove(Object o)
从此集合中移除指定对象
int size()
返回此集合的元素个数
Object[] toArray()
将此集合转换成数组
Iterator iterator()
返回集合中的元素的迭代器
遍历
增强for
Collection collection = new ArrayList<>();
collection.add("zhangsan");
collection.add("lisi");
collection.add("wangwu");
collection.add("zhaoliu");
for (Object object : collection) {
System.out.println(object);
}
Iterator
Collection collection = new ArrayList<>();
collection.add("zhangsan");
collection.add("lisi");
collection.add("wangwu");
collection.add("zhaoliu");
// 迭代器(专门用来遍历集合的一种方式)
Iterator iterator = collection.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
// 移除iterator迭代器当前正在指向的元素
iterator.remove();
// 使用collection.remove(iterator.next())方法会抛出并发修改异常
// 因为iterator正在collection集合对象中的元素,此时collection也在操作自身的元素
// 就会产生异常
// collection.remove(iterator.next());
}
List接口
有序,有下标,元素可以重复
Collection有的方法,List都有,不在赘述,下面看独有的方法
ListIterator listIterator()
返回List的ListIterator
ListIterator listIterator(int index)
返回List从指定索引开始的ListIterator,ListIterator包含指定索引的元素
int indexOf(Object o)
返回在List中指定对象第一次出现的索引
List subList(int fromIndex,int toIndex)
返回指定索引区间的元素子集合,左闭右开
遍历
增强for,迭代器Iterator
Collection能够使用这两种方式,List也就能使用
for遍历
因为有序,有下标
列表迭代器ListIterator
和Iterator区别:
可以向前或者向后遍历(Iterator只能向后)
可以添加、删除、修改元素(Iterator只有删除元素)
List list = new ArrayList<>();
list.add("zhangsan");
list.add("lisi");
list.add("wangwu");
list.add("zhaoliu");
ListIterator listIterator = list.listIterator();
// 从前向后迭代
while(listIterator.hasNext()){
System.out.println(listIterator.next()); // zhangsan lisi wangwu zhaoliu
}
// 从后向前迭代
while(listIterator.hasPrevious()){
System.out.println(listIterator.previous()); // zhaoliu wangwu lisi zhangsan
}
List实现类
ArrayList
jdk1.2
数组结构实现,查询操作快,更新操作慢
运行效率快,线程不安全
Vector
jdk1.0
数组结构实现,查询操作快,更新操作慢
运行效率慢,线程安全
LinkedList
jdk1.2
链表(双链表)结构实现,查询慢,更新快
运行效率快,线程不安全
ArrayList与LinkedList存储结构
List实现
ArrayList
ArrayList拥有List接口和Collection接口所有抽象方法(接口在jdk1.8之后增加了默认方法)
数组结构实现,查询操作快,更新操作慢
运行效率快,线程不安全
可以存在多个null值
remove方法
当进行删除集合中的元素对象操作时,集合中的每个元素和指定的目标元素匹配,如果相等就删除。匹配的依据就是通过集合中元素类型的equals。可以通过重写equals方法实现自己想要的匹配效果
// Person类两个属性 String name; int age;
ArrayList<Person> arrayList = new ArrayList<>();
arrayList.add(new Person("zhangsan",13));
arrayList.add(new Person("lisi",14));
arrayList.add(new Person("wangwu",15));
// 这种情况下,删除无效,因为调用的equals方法是Object的equals方法,比较的是对象的地址
// 显然,重新new了一个Person,引用地址匹配不到此时集合中的各个元素的地址
arrayList.remove(new Person("zhngsan",13));
// [Person{name='zhangsan', age=13}, Person{name='lisi', age=14}, Person{name='wangwu', age=15}]
System.out.println(arrayList);
// Person类重写equals方法,达到自己想要实现的匹配效果
@Override
public boolean equals(Object obj) {
// 比较引用地址是否相等
if(this==obj){
return true;
}
// 判断obj是否为null
if(obj==null){
return false;
}
// 判断是否是同一个类型
if(obj instanceof Person){
// 类中属性分别进行比较
Person person = (Person) obj;
if(this.name.equals(person.name) && this.age==person.age){
return true;
}
}
return false;
}
// 重写了Person类中的equals方法后,删除成功
arrayList.remove(new Person("zhangsan",13));
// [Person{name='lisi', age=14}, Person{name='wangwu', age=15}]
System.out.println(arrayList);
遍历
Collection接口的遍历方式:增强for、迭代器Iterator
List接口的遍历方式:for、列表迭代器ListIterator
源码分析
private static final int DEFAULT_CAPACITY = 10; 默认容量大小是10
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 空数组
transient Object[] elementData; 存放元素的数组
private int size; 实际元素个数
无参构造方法
// 无参构造,当调用无参构造创建对象后,elementData是个空数组,此时没有元素
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public boolean add(E e) 添加元素的方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
添加第一个元素,数组的容量从0扩大到10
之后的扩容规则是以此时数组的1.5被扩容
如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容则直接扩容elementData为1.5倍
Vector
jdk1.0
数组结构实现,查询操作快,更新操作慢
运行效率慢,线程安全
遍历
Collection:增强for、迭代器Iterator
List:普通for、列表迭代器ListIterator
自己独有:枚举器Enumeration
枚举器Enumeration:
Vector vector = new Vector();
vector.add("zhangsan");
vector.add("lisi");
vector.add("wangwu");
// 枚举器遍历
Enumeration elements = vector.elements();
while(elements.hasMoreElements()){
System.out.println((String)elements.nextElement());
}
// 判断操作
System.out.println(vector.contains("zhangsan")); // true
System.out.println(vector.isEmpty()); // false
// 其他方法
System.out.println(vector.firstElement()); // zhangsan
System.out.println(vector.lastElement()); // wangwu
System.out.println(vector.elementAt(1)); // lisi
和ArrayList比较
LinkedList
jdk1.2
链表(双链表)结构实现,查询慢,更新快
运行效率快,线程不安全
遍历
Collection接口的遍历方式:增强for、迭代器Iterator
List接口的遍历方式:for、列表迭代器ListIterator
源码分析
transient int size = 0; 实际元素个数
transient Node first; 头节点
transient Node last; 尾节点
无参构造方法
public LinkedList() {
}
public boolean add(E e) 添加元素的方法
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++;
}
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;
}
}
List 集合选择
ArrayList和LinkedList比较
ArrayList和Vector比较
泛型
Java 泛型是JDK1.5引入的一个新特性,本质是把参数化类型 ,即类型可以作为参数传递。
常见形式有泛型接口、泛型类、泛型方法。
语法:<T,….> T称为类型占位符 ,表示一种引用类型。
好处:提高代码的重用性;防止类型转换异常,提高代码的安全性。
泛型类
注意点:
泛型只能是引用数据类型
不同的泛型类型对象之间不能相互复制
class MyGeneric<T> {
// 使用泛型T
// 创建变量
T t;
//泛型不能new,因为不确定T是何种类型,所有就不能确定调用的构造方法一定存在
//T t = new T();
// 泛型作为方法的参数
public void show(T t) {
System.out.println(t);
}
public void setT(T t) {
this.t = t;
}
public T getT() {
return t;
}
}
public static void main(String[] args) {
/**
* 1.泛型只能是引用数据类型
* 2.不同的泛型类型对象之间不能相互复制
*/
// 使用泛型类创建对象
// 此时指定泛型是String类型
MyGeneric<String> myGeneric = new MyGeneric<>();
myGeneric.setT("zhangsan");
System.out.println(myGeneric.getT()); // zhangsan
// 此时指定泛型是Integer类型
MyGeneric<Integer> myGeneric2 = new MyGeneric<>();
myGeneric2.setT(100);
System.out.println(myGeneric2.getT()); // 100
//不成立,因为myGeneric的类型泛型是String,myGeneric2的类型泛型是Integer
//myGeneric=myGeneric2;
}
泛型接口
在泛型接口中,不能定义泛型变量
因为接口中的变量默认使用public static final 修饰,要求接口中的变量必须是静态常量。常量就必须赋值,但是泛型是不确定的类型,无法调用构造方法创建对象
interface MyGenericInterface<T> {
String name = "zhangsan";
/**
* 在泛型接口中,不能定义泛型变量
* 因为接口中的变量默认使用public static final 修饰,要求接口中的变量必须是静态常量
* 常量就必须赋值,但是泛型是不确定的类型,无法调用构造方法创建对象
*/
// T t =new T(); 不能这样定义
// 泛型作为抽象方法的参数和返回值
T show(T t);
}
/**
* 实现类1,实现泛型接口
*/
class MyGenericInterfaceImpl implements MyGenericInterface<String> {
@Override
public String show(String s) {
System.out.println(s);
return s;
}
}
/**
* 实现类2,实现泛型接口
* 将实现类也定义为泛型类,实现类所用的泛型和实现的接口的泛型是一致的
*/
class MyGenericInterfaceImpl2<T> implements MyGenericInterface<T> {
@Override
public T show(T t) {
return null;
}
}
public static void main(String[] args) {
MyGenericInterface myGenericInterface = new MyGenericInterfaceImpl();
System.out.println(myGenericInterface.show("zhangsan")); // zhangsan
MyGenericInterface myGenericInterface2 = new MyGenericInterfaceImpl2<String>();
System.out.println(myGenericInterface2.show("lisi")); // lisi
}
泛型方法
语法
访问修饰符 <T> 返回值类型 方法名(参数列表)
泛型方法的T只是定义了这个方法的泛型,和泛型类的T没关系(各规定各的)
泛型方法的T由调用时传递的参数类型决定,如果泛型方法需要用到泛型,就必须指定参数类型
class MyGenericMethod<T>{
private T t;
// 非泛型方法,T类型和MyGenericMethod<T>的T类型一致
public T show3(T t){
System.out.println("泛型方法2");
return t;
}
// 泛型方法1,T和MyGenericMethod<T>的T各规定各的
public <T> void show(T t){
System.out.println("泛型方法1:"+ t);
}
// 泛型方法2
// T作为返回类型,T和MyGenericMethod<T>的T各规定各的
public <T> T show2(T t){
System.out.println("泛型方法2" + t);
return t;
}
}
public static void main(String[] args) {
// 非泛型方法规定的类型只能是String类型,和泛型类规定的一致
stringMyGenericMethod.show3("zhangsan");
// 泛型类规定的泛型是String类型
MyGenericMethod<String> stringMyGenericMethod = new MyGenericMethod<>();
// 泛型方法1规定的类型是Integer类型
stringMyGenericMethod.show(10);
// 泛型方法1规定的类型是Person类型
stringMyGenericMethod.show2(new Person());
}
Set接口
无序,无索引,不能重复(所以最多包含一个null)
Set接口中的方法都是从Collection中继承过来的
遍历
增强for、迭代器Iterator
Set实现
HashSet【重点】
存储结构
哈希表(数组+链表)
jdk1.8之后,数组+链表+红黑树
底层机制
原理:
HashSet 底层是 HashMap
添加一个元素时,先得到hash值 -会转成-> 索引值
找到存储数据表table,看这个索引位置是否已经存放的有元素
如果没有,直接加入
如果有,调用 equals 比较,如果相同,就放弃添加,如果不相同,则添加到最后
在Java8中,如果一条链表的元素个数到达 TREEIFY THRESHOLD(默认是 8),并且table的大小 >=MIN TREEIFY CAPACITY(默64)就会进行树化(红黑树),如果链表的个数到达默认的8个,但是数组的长度没有超过64的话,就会对数组进行扩容(以当前数组的2倍进行扩容)
底层机制说明:
Hashset民层是HashMap,第一次添加时,table 数组扩容到16,临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12
如果table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,新的临界值就是32*0.75 = 24,依次类推
在Java8中,如果一条链表的元素个数到达 TREEIFY THRESHOLD(默认是 8)并且table的大小 >=MIN TREEIFY CAPACITY(默认64)就会进行树化(红黑树),否则仍然采用数组扩容机制
这里的12指的是加到HashSet中的元素的个数,在第一个元素添加后,数组扩容到16个长度,加载因子是12。那么当添加到第13个元素的时候,超过了加载因子12,此时数组就会按照当前数组长度的2倍进行扩容,并改变加载因子。
所以数组扩容的时机有两个方面
当数组长度没有达到64,数组索引的对应位置的链表元素个数超过8个,会扩容。或者添加的元素总数超过加载因子,也会扩容
当数组长度达到64,只有添加的元素的总数超过加载因子,才会进行扩容
底层源码分析
// HashSet无参构造,就是创建了一个HashMap对象
public HashSet() {
map = new HashMap<>();
}
=========================================================================================
// 添加元素add方法,调用的是HashMap的map.put(e, PRESENT)
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
=========================================================================================
// HashMap的put(K key, V value) {
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
=========================================================================================
// 通过被添加元素的hashCode()方法获得一个整数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 查看putVal(hash(key), key, value, false, true);
// hash:通过hash(Object key)方法获得得那个整数
// key:添加得元素
// value:new Object()获得得一个Object类型得对象
// onlyIfAbsent:传递得是false
// evict:传递得是true
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 声明局部变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判断如果table数组是null或者长度为0,数组调用扩容方法resize()进行扩容,第一次扩容后得长度是16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通过元素哈希值得到得那个整数hash,确定这个被添加元素对应数组得索引位置
// 如果这个索引对应得位置没有元素,则将这个元素添加到这个位置上
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果这个索引得对应位置有元素
else {
Node<K,V> e; K k;
// 判断这个索引位置上得第一个元素是否和添加得元素相同,如果相同,将第一个元素赋值给变量e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果不相同,判断这个索引位置上得第一个元素类型是否是TreeNode类型(其实就是判断这个索引位置上得链 // 表是否转换为了红黑树),如果成为了红黑树,就将这个元素放到树节点上
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果这个索引位置的第一个元素和添加的元素不重复,并且这个索引位置上的元素还是链表结构(没有成 // 为红黑树结构)
else {
// 将这个索引位置上的元素进行遍历(注意,因为已经与第一个元素判断过,不与第一个元素重复)
// 所以这里直接从第二个元素开始匹配,进行判断。
for (int binCount = 0; ; ++binCount) {
// 如果此元素的下一个元素是null值,证明这个这个元素已经是链表的末尾位置,直接将要添加的元
// 素添加到这个元素的后面,此时要添加的元素就是在链表的末尾位置
if ((e = p.next) == null) {
// 将元素添加到链表的末尾
p.next = newNode(hash, key, value, null);
// 此时这个链表的元素个数是9个(已经超过8(阈值)个),进入treeifyBin(tab, hash);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 简单来说,这个方法先判断此时数组的长度是否到达64
// 如果没有达到,则数组进行扩容
// 如果达到,就将这个链表转换为红黑树(TreeNode)
treeifyBin(tab, hash);
break;
}
// 判断链表中此元素是否和要添加的元素相同,如果相同,元素不添加,结束循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 如果此元素的下一个元素不是null,并且还不重复
// 将下一个元素赋值给p,与要添加的元素进行匹配判断,进行下一次循环
p = e;
}
}
// 如果存在相同的元素,则返回new Object()对象
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 每添加成功一个元素,size就+1
// 如果数组中添加总的元素个数到达数组扩容的阈值(第一次数组扩容到16,阈值是12(16*0.75).第二次数组扩容 // 到32,阈值24(32*0.75)),数组就进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
// 元素添加成功,返回null
return null;
}
存储过程
根据HashCode计算元素的存储位置
如果计算出来的存储位置没有存储对象,那么就直接存储
如果有,就调用equals方法,如果equals方法返回true,则认为重复拒绝写入
否则,形成链表。
Set<Person> personHashSet = new HashSet<>();
personHashSet.add(new Person("zhangsan",3));
personHashSet.add(new Person("lisi",4));
personHashSet.add(new Person("wangwu",5));
// 此时据可以添加成功
// 因为对象的hashcode(决定数组的存储位置)值和16进制的引用地址(比较两个对象是否相等)都不一样
// 所以HashSet认为是不同的数据
personHashSet.add(new Person("zhangsan",3));
// 如果认定同名和同龄的两个对象就是重复数据,不能添加成功
// 那就要重写Person的hashCode方法,保证同名和同龄的Person对象能在同一个数组位置
// 还要重写equals方法,保证同名和同龄的对象就是相同的对象
// 这样HashSet将不会添加同名和同龄的Person对象,因为视为重复数据
class Person{
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int hashCode() {
// 31 是个质数,减少散列冲突
// 31 提高执行效率 31 * i = (i<<5)-i
final int prime = 31;
int result = 1;
result = prime * result + this.age;
result = prime * result + ((name == null) ? 0 : name.hashCode() );
return result;
}
@Override
public boolean equals(Object obj) {
// 比较引用地址是否相等
if(this==obj){
return true;
}
// 判断obj是否为null
if(obj==null){
return false;
}
// 判断是否是同一个类型
if(obj instanceof Person){
// 类中属性分别进行比较
Person person = (Person) obj;
if(this.name.equals(person.name) && this.age==person.age){
return true;
}
}
return false;
}
}
// PerSon重写完hashCode和equals之后,添加同名和统领的数据
personHashSet.add(new Person("zhangsan",3)); // 添加失败
LinkedHashSet
LinkedHashSet是HashSet的子类;
LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表;
LinkedHashSet根据元素的hashcode值来决定元素的存储位置,同时使用链表维护元素的次序,是元素看起来以插入顺序保存;
LinkedHashSet不允许添加重复元素.
源码分析
// 示例
LinkedHashSet linkedHashSet=new LinkedHashSet();
// LinkedHashSet的无参构造,调用了父类HashSet的有参构造方法
public LinkedHashSet() {
super(16, .75f, true);
}
// 父类的有参构造,创建了LinkedHashMap对象,LinkedHashMap是HashMap的子类
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
// 所以总结来说,创建LinkedHashSet对象后,这个对象的实例属性map指向的对象是LinkedHashMap
=========================================================================================
// 我们再来看看添加元素时时如何保证元素的添加顺序的,进入add方法
linkedHashSet.add("ldjfla");
// map指向的是LinkedHashMap,如果重写了父类HashMap的put方法,这里调用的是子类的put方法,如果没有重写,
// 调用的是父类的put方法.我们点进去查看put方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// 方法没有被重写,使用的是父类的put方法,查看putVal方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// putVal方法也没有被子类重写,所以现在可以得出:添加逻辑和HashSet的添加逻辑是一样的,因为调用的都是父类的
// 方法,既然添加的逻辑一样,那又是如何保证元素的插入顺序的。我们看putVal方法里面的newNode方法
newNode(hash, key, value, null);
// newNode方法被子类重写了,看重写的方法
// 可以看到,数组中存储元素的类型是LinkedHashMap.Entry。而数组的类型是Node。
// 我们查看LinkedHashMap.Entry类
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
// 可以看到LinkedHashMap.Entry是HashMap.Node的子类。
// 里面的数据不仅有Node保存的数据,还有一个指向前后节点的数据before和after
// 从这里我们看出来,就是通过before和after,来指向前后节点,记录元素添加的先后顺序
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
// 我们看看具体是怎样记录顺序的
// newNode方法中有个linkNodeLast方法
// 可以得到,当第一个元素添加进来时,head节点和tail都指向了第一个元素
// 当第二个元素添加时,head指向的还是第一个元素tail指向了第二个元素,第一个节点的after指向第二个元素,第二
// 个元素的before指向第一个元素
// 由此推论,当第三个元素进来时,head还是指向第一个添加的元素,tail指向第三个元素,第三个元素的before指向
// 第二个元素,第二个元素的after指向第三个元素
// 后面添加的元素以此类推....,通过这个前后元素顺序的指向,保证了元素的插入顺序
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
TreeSet
无下标、不能重复、默认的情况下是无序的,但是我们可以指定比较器或元素类型实现Comparable接口,实现元素的有序
存储结构
红黑树
源码分析
// 示例:无参构造方法
TreeSet<String> objects = new TreeSet<>();
// 调用了里面的有参构造,创建了一个TreeMap对象
// 从这里我们可以知道,TreeSet的底层是TreeMap
public TreeSet() {
this(new TreeMap<E,Object>());
}
// 查看有参构造,发现m指向的对象就是无参构造中创建的TreeMap对象
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
=========================================================================================
// 示例:添加元素的方法
objects.add("a");
// 可以看到,内部调用的是treeMap的put方法
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
// 我们一步一步分析
public V put(K key, V value) {
Entry<K,V> t = root;
// 如果这个根结点没有元素,比如第一次添加元素时,根节点此时就是null
if (t == null) {
// 这时会调用compare方法
compare(key, key); // type (and possibly null) check
// 我们查看compare方法做了什么
final int compare(Object k1, Object k2) {
// 如果我们没有再构造方法中指定Comparator比较器,这是就会用Comparable比较器比较元素的大小
// (Comparable<? super K>)k1 涉及到了元素的强制类型转换
// 所以基于这一点,就要求添加的元素类型必须实现Comparable接口,重写compareTo方法
// 如果我们指定了Comparator比较器,就会用指定的比较器比较元素的大小
return comparator==null ?
((Comparable<? super K>)k1).compareTo((K)k2)
:
comparator.compare((K)k1, (K)k2);
}
// 根节点就是我们添加的这个元素
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
// 如果指定了Comparator比较器,就是我们指定的去比较元素的大小,确定元素在树中的添加位置
if (cpr != null) {
// 从根节点开始,被加添的元素依次去和树中的元素进行比较
// 被添加的元素小于树中的某个节点的话,放在左边,否则放在右边
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
// 如果经过比较器比较后,返回0,代表存在重复的元素,此时替换这个节点的value值,但是key的
// 值是添加不进去的
return t.setValue(value);
} while (t != null);
}
// 如果没有指定Comparator比较器,就用Comparable去比较元素的大小,确定元素在树中的添加位置
// 这也在要求被添加的元素类型实现Comparable接口
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
// 创建一个新的节点,保存被添加的元素
Entry<K,V> e = new Entry<>(key, value, parent);
// 将新添加的元素放到树中的指定位置
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
排列
Comparable接口
集合元素实现compareTo方法
class Person implements Comparable{
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 +
'}';
}
// 先比较姓名,如果姓名相同,在比较年龄,如果年龄也相同,则认为是重复元素,无法添加
@Override
public int compareTo(Object o) {
Person person = (Person)o;
int i1 = this.name.compareTo(person.name); // 因为前一个对象主动比较后一个对象,所以顺序是前小后大
int i2 = this.age - person.age; // 前小后大的顺序,如果名字一样,年龄为3在前,年龄为4在后
return i1==0 ? i2 : i1;
}
}
public static void main(String[] args) {
TreeSet<Person> treeSet = new TreeSet<>();
// Person的compareTo方法返回0则认为是重复元素
treeSet.add(new Person("lisi",3));
treeSet.add(new Person("lisi",4));
treeSet.add(new Person("lisi",5));
treeSet.add(new Person("lisi",4)); // 认为是重复元素,不能添加
treeSet.add(new Person("lisi",5)); // 因为名字一样,比较年龄,前小后大,所以年龄为4在前,5在后
// [Person{name='lisi', age=3}, Person{name='lisi', age=4}, Person{name='lisi', age=5}]
System.out.println(treeSet);
}
Comparator接口
实现定制比较
通过自定义比较器确定是否是重复元素,并排序。
实现compare方法
public static void main(String[] args) {
// 通过比较器
TreeSet<Person> perosnSet = new TreeSet<>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
int i1 = o2.getName().compareTo(o1.getName());// 后一个对象主动比较前一个对象,所以顺序是前大后小
int i2 = o2.getAge() - o1.getAge(); // 前大后小的顺序,如果名字一样,年龄为4在前,年龄为3在后
return i1==0 ? i2 : i1;
}
perosnSet.add(new Person("lisi",3));
perosnSet.add(new Person("lisi",4));
perosnSet.add(new Person("lisi",5));
perosnSet.add(new Person("lisi",5));
// [Person{name='lisi', age=5}, Person{name='lisi', age=4}, Person{name='lisi', age=3}]
System.out.println(perosnSet);
}
Comparable和Comparator
区别:
Comparable被集合元素实现,Comparator被集合构造器参数实现
Comparator比Comparable优先
// 根据字符才能的自然顺序排序,a→z
TreeSet<String> stringTreeSet1 = new TreeSet<>();
stringTreeSet1.add("aaaaaaaaa");
stringTreeSet1.add("bbbbbbb");
stringTreeSet1.add("ccccc");
TreeSet<String> stringTreeSet2 = new TreeSet<>(new Comparator<String>() {
// 根据集合元素中的字符串长度排序
// 长度短的在前
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
stringTreeSet2.add("aaaaaaaaa");
stringTreeSet2.add("bbbbbbb");
stringTreeSet2.add("ccccc");
System.out.println(stringTreeSet1); // [aaaaaaaaa, bbbbbbb, ccccc]
System.out.println(stringTreeSet2); // [ccccc, bbbbbbb, aaaaaaaaa]
Map接口
特点
Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value
Map 中的 key 和 value 可以是任何引用类型的数据,会封装到HashMap$Nod对象中
Map 中的 key 不允许重复,Map 中的 value 可以重复
Map 的key 可以为 null, value 也可以为null ,注意 key 为null, 只能有一个。value 为null ,可以多个
常用String类作为Map的 key
key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的。value
判断重复数据的依据是Key的hashCode和equals方法,和Value无关
Map存放数据的key-value,一对 k-v 是放在一个Node中的,又因为Node 实现了 Entry 接口,有些书上也说 一对k-v就是一个Entry(如图)
常用方法
V put(K key,V value)
将键值对存入到Map中。Key 重复会覆盖掉原值
Object get(Object key)
根据键获取值
Set keySet()
返回所有Key
Collection values()
返回包含所有值的Collection集合
Set<Map.Entry<K,V>> entrySet()
键值匹配的Set集合
public boolean containsKey(Object key)
判断是否存在指定Key值
public boolean containsValue(Object value)
判断是否存在指定Value值
遍历
Map接口的六大遍历方式
entrySet效率高于keySet
keySet方法
// (1)增强for
for (Object key : map.keySet()){
System.out.println(key + "-" + map.get(key));
}
// (2)迭代器
Iterator iterator = map.keySet().iterator();
while (iterator.hasNext()) {//快捷键itit
Object key = iterator.next();
System.out.println(key + "-" + map.get(key));
}
values方法
Collection values = map.values();
//这里可以使用所有collections使用的遍历方法
//(1)增强for
for (Object value:
values) {
System.out.println(value);
}
//(2)迭代器
Iterator iterator1 = values.iterator();
while (iterator1.hasNext()) {
Object next = iterator1.next();
System.out.println(next);
}
entrySet方法
//(1)增强for
Set entrySet = map.entrySet();
for (Object o:
entrySet) {
//将entry转成Map.Entry
Map.Entry entry = (Map.Entry) o;
System.out.println(entry.getKey() + "-" + entry.getValue());
}
//(2)迭代器
Iterator iterator2 = entrySet.iterator();
while (iterator2.hasNext()) {
Object next = iterator1.next();
Map.Entry entry = (Map.Entry) next;
System.out.println(entry.getKey() + "-" + entry.getValue());
}
实现类
HashMap
JDK1.2,线程不安全,运行效率快;允许用null作为Key或Value
Hashtable
JDK1.0,线程安全,运行效率慢;不允许用null作为Key或Value
Properties
Hashtable 子类,要求Key和Value都必须String;通常用于资源文件的读写
TreeMap
实现了SortedMap接口(是Map的子接口),可以对Key自动排序
Map实现
HashMap(重点)
JDK1.2,线程不安全,运行效率快;允许用null作为Key或Value
存储结构
哈希表(数组 + 链表 + 红黑树(jdk1.8))
存储过程
根据Key对象的hashCode和eaqual方法进行重复数据判断,决定是否添加数据
public static void main(String[] args) {
HashMap<Student, String> hashMap = new HashMap<>();
hashMap.put(new Student("zhangsan",3),"北京");
hashMap.put(new Student("lisi",4),"北京");
hashMap.put(new Student("wangwu",5),"北京");
// 可以添加成功
// 因为Map是根据Key判读是否是重复数据,和Value无关
// HashMap数据结构是哈希表(数组+链表+红黑树),Student没有重写hashCode方法和equals方法
// 此时是使用Student对象的16进制地址作为判断重复数据的依据
hashMap.put(new Student("zhangsan",3),"河北");
// Student重写hashCode方法和equals方法之后,不能添加成功
// 因为通过hashCode方法和equals方法,认为只要是Student对象的名字和学号一样,就认为是重复数据,不能添加
hashMap.put(new Student("zhangsan",3),"河北");
}
class Student{
private String name;
private int no;
public Student() {
}
public Student(String name, int no) {
this.name = name;
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", no=" + no +
'}';
}
@Override
public int hashCode() {
return Objects.hash(getName(), getNo());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student student = (Student) o;
return getNo() == student.getNo() && Objects.equals(getName(), student.getName());
}
}
源码分析
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
hashMap初始容量大小
static final int MAXIMUM_CAPACITY = 1 << 30;
hashMap的数组最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认加载因子
static final int TREEIFY_THRESHOLD = 8;
jdk1.8 当链表长度大于8时,调整为红黑树
static final int UNTREEIFY_THRESHOLD = 6;
jdk1.8 当链表长度小于6时,调整为链表
static final int MIN_TREEIFY_CAPACITY = 64;
jdk1.8 当链表长度大于8时,并且元素大于等于64时,调整为红黑树
transient Node<K,V>[] table
哈希表中的数组
transient int size ;
元素个数
HashMap刚创建没添加元素时,table是null,为了节省空间,当添加第一个元素时,table容量调整为16.
当元素个数大于阈值(16*0.75=12)时,会进行扩容,扩容后大小为原来的2倍。目的是减少调整元素的个数。
jdk1.8 当每个链表长度大于8,并且数组元素个数大于等于64时,会调整为红黑树,目的提高执行效率。
jdk1.8 当链表长度小于6时,调整为链表。
jdk1.8以前,链表是头插入,jdk1.8以后是尾插入。
拓展:
HashSet 内部用的就是HashMap。
HashTable
JDK1.0,
线程安全,运行效率慢;
不允许用null作为Key或Value
存储结构
哈希表
源码分析
初始化容量大小为11,
加载因子0.75
阈值=数组容量大小*加载因子(所以第一个扩容后,阈值=8)
扩容:按照自己的扩容机制进行 ,执行addEntry(...)方法
当count >= threshold时,进行扩容, 新容量为原先容量*2 +1
话不多说,直接上源码
// 示例
Hashtable<Object, Object> objectObjectHashtable = new Hashtable<>();
// 查看无参构造,发现调用的是有参构造,查看有参构造
public Hashtable() {
this(11, 0.75f);
}
// 有参构造
// initialCapacity 数组初始化的容量大小,11
// loadFactor 加载因子,0.75
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
// 创建一个容量大小为11的数组
table = new Entry<?,?>[initialCapacity];
// 根据数组的容量大小和加载因子的乘积计算数组扩容的阈值,首次扩容阈值为8
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
// 示例,查看put方法
objectObjectHashtable.put("afs","lakfsjd");
// 添加元素的方法
public synchronized V put(K key, V value) {
// Make sure the value is not null
// 这里值(value)为null,抛出异常
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
// 在这里,如果传入的键(key为null),就会抛出空指针异常,所以hashtable的键和值都不能为null
int hash = key.hashCode();
// 通过键的hash和数组的长度计算索引
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// 获取该索引位置的第一个元素
Entry<K,V> entry = (Entry<K,V>)tab[index];
// 如果该索引位置的第一个元素不为null,就遍历该索引位置的链表上的每个元素,判断是否重复
// 如果有重复的元素,则替换这个重复元素的value值,返回这个重复元素之前的value值
// 要添加的元素添加失败
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
// 如果没有重复的元素,则调用添加元素的方法,进行元素的添加操作
addEntry(hash, key, value, index);
return null;
}
// 查看添加元素的方法
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
// 判断当前集合中的元素个数是否超过阈值,如果超过阈值,则进行扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
// 获取该索引位置的第一个元素
Entry<K,V> e = (Entry<K,V>) tab[index];
// 将要添加的元素添加到该索引位置
// 新添加的元素成为该索引位置的第一个元素,新添加的元素的next属性指向原索引位置的第一个元素
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
// 查看扩容的方法
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
// 扩容规则:当前数组容量*2+1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
// 数组进行扩容,获取一个新数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
// 计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 将旧数组的数据放到新的数组当中
// 这块的源码中,循环的使用很有意思,我们来分析一下
// 外层循环
// i-->0,变量i先进行>0的判断,然后再-1,最后进入循环
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
与HashMap对比
Properties
HashTable的子类,要求key和value都是String。通常用于配置文件的读取
Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的开式来保存数据。
他的使用特点和Hashtable类似
Properties 还可以用于从 xxx.properties 文件中,加载数据到Properties类对象并进行读取和修改
说明: 工作后 xxx.properties 文件通常作为配置文件,这个知识点在IO流举例,有兴趣可先看文章
TreeMap
实现了SortedMap接口(是Map的子接口),可以对Key按照指定的比较器进行排序
集合中Key元素实现Comparable接口,重写compareTo
或者
调用TreeMap构造方法传递比较器对象(实现Comparator接口,重写compare方法)
判断数据是否重复的依据是集合中Key元素的compareTo方法或者compare方法
实现Comparable
public static void main(String[] args) {
TreeMap<Student, String> treeMap = new TreeMap<>();
treeMap.put(new Student("zhangsan",3),"北京");
treeMap.put(new Student("lisi",4),"北京");
treeMap.put(new Student("wangwu",5),"北京");
treeMap.put(new Student("wangwu",5),"河北"); // 添加失败
// {Student{name='lisi', no=4}=北京, Student{name='wangwu', no=5}=河北,
// Student{name='zhangsan', no=3}=北京}
System.out.println(treeMap);
}
class Student implements Comparable{
private String name;
private int no;
...
...
...
@Override
public int compareTo(Object o) {
Student student = (Student)o;
int i1 = this.getName().compareTo(student.getName());
int i2 = this.getNo() - student.getNo();
return i1 == 0 ? i2 : i1;
}
}
实现Comparator
TreeMap<Student, String> treeMap = new TreeMap<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
int i1 = o1.getName().compareTo(o2.getName());
int i2 = o1.getNo() - o2.getNo();
return i1 == 0 ? i2 : i1;
}
});
treeMap.put(new Student("zhangsan",3),"北京");
treeMap.put(new Student("lisi",4),"北京");
treeMap.put(new Student("wangwu",5),"北京");
treeMap.put(new Student("wangwu",5),"河北"); // 添加失败
// {Student{name='lisi', no=4}=北京, Student{name='wangwu', no=5}=河北,
// Student{name='zhangsan', no=3}=北京}
System.out.println(treeMap);
遍历
keySet方法和entrySet方法
存储结构
红黑树
拓展:
HashSet 内部用的就是HashMap。
java集合对比
java集合对比
List集合
ArrayList
数据结构:数组结构实现
创建对象时:调用无参构造,数组是空数组{}
第一次添加数据时:初始化容量是10
之后的扩容机制:按照当前数组容量的1.5倍进行扩容。
如果是有参构造,初始化容量是指定的容量,之后的扩容机制就是按照此时数组容量的1.5倍进行扩容
扩容原理:调用c++的程序,进行数组的复制操作。最底层是System.arraycopy的native方法进行数组复制
线程不安全,查询效率高,更新效率低,内存连续
Vector
数据结构:数组结构实现
无参构造创建对象时:对象的容量大小是10
有参构造创建对象时:数组按照指定的容量进行扩容
扩容机制:按照当前数组容量的2倍扩容
扩容原理:和ArrayList的原理一样,采用数组复制的方式进行扩容
线程安全,查询快,更新慢,内存连续
LinkedList
数据结构:双向链表
有参或者无参构造方法创建对象时:因为是双向链表结构,所以不会有初始容量一说
扩容原理:存储元素使用的是LinkedList的静态内部类Node,LinkedList有两个属性first和last,都是Node类型,每个Node对象中的prev和next属性都指向了上一个元素和下一个元素。
线程不安全,查询数据慢(使用二分法查找数据),更新速度快,内存不连续
Set集合
HashSet
数据结构:数组+链表+红黑树
底层实现:HashMap实现
无参构造创建对象时:此时数组是null
加载因子:0.75
第一次添加数据时:数组扩容到16,阈值为16*0.75=12
阈值:此时数组容量*加载因子
扩容机制:
当数组的某个索引位置链表长度大于8(当添加第9个元素时),并且数组的容量小于64时,进行扩容
当数组容量大于阈值时,进行扩容
按照当前数组容量的2倍进行扩容
转换红黑树:当链表长度大于8,并且数组容量大于等于64时,则链表转换为红黑树
线程不安全
LinkedHashSet
数据结构:数组+双向链表+红黑树
底层实现:LinkedHashMap
在创建对象时,第一次添加元素时,加载因子,阈值,扩容机制,转换红黑树等等和HashSet一样
类之间的关系:
LinkedHashSet是HashSet的子类
HashSet底层是HashMap,LinkedHashSet底层是LinkedHashMap
LinkedHashMap是HashMap的子类
HashMap存数据使用的是其静态内部类Node,LinkedHashMap使用的是他的静态内部类Entry
Entry是Node的子类,Node里面的next属性可以保证单向链表(数组的索引位置上的链表)
Entry里面的before和after属性可以记录元素的先后插入顺序,保证的是插入顺序的双向链表
LinkedHashMap里面有head和tail属性,记录的是第一个插入的元素和最后一个插入的元素
线程不安全
Map集合
HashMap
数据结构:数组+链表+红黑树
键和值都可以为null
在创建对象时,第一次添加元素时,加载因子,阈值,扩容机制,转换红黑树等等和HashSet一样
线程不安全
HashTable
数据结构:数组+链表
无参构造创建对象时:数组容量是11
加载因子:0.75
阈值:当前数组容量*加载因子
扩容机制:
当元素个数大于等于阈值时(第一次扩容时,阈值是8,所以当添加第九个元素时,扩容),按照当前数组容量的*2+1方式进行扩容
线程安全
开发中的集合选择
在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:
先判断存储的类型 (一组对象[单列]或一组键值对[双列])
一组对象:Collection接口
允许重复: List
增删多: LinkedList [底层维护了一个双向链表]
改查多: ArrayList [底层维护 Object类型的可变数组]
不允许重复: Set
无序: HashSet [底层是HashMap ,维护了一个哈希表 即(数组+链表+红黑树)]
排序: TreeSet
插入和取出顺序一致: LinkedHashSet,维护数组+双向链表
一组键值对: Map
键无序: HashMap [底层是: 哈希表 jdk7: 数组+链表,jdk8: 数组+链表+红黑树]
键排序: TreeMap
键插入和取出顺序一致: LinkedHashMap
读取文件 Properties
Collections工具类
集合工具类,定义了集合除存取以外的集合常用方法
public static void reverse(List<?> list)
反转集合中元素的顺序
public static void shuffle(List<?> list)
随机重置集合元素的顺序
public static <T extends Comparable<? super T>> void sort(List list)
升序排序(元素类型必须实现Comparable接口)
二分法查找
binarySearch
List<Integer> arrayList = new ArrayList<>();
arrayList.add(324);
arrayList.add(543);
arrayList.add(4543);
arrayList.add(13);
arrayList.add(13242);
arrayList.add(321421);
int i = Collections.binarySearch(arrayList, 543);
System.out.println(i); // 1
List<Student> arrayList = new ArrayList<Student>();
arrayList.add(new Student("zhangsan",3));
arrayList.add(new Student("lisi",4));
arrayList.add(new Student("wangwu",5));
arrayList.add(new Student("wangwu",4));
// [Student{name='zhangsan', no=3}, Student{name='lisi', no=4},
// Student{name='wangwu', no=5}, Student{name='wangwu', no=4}]
System.out.println(arrayList); // 按照添加顺序输出
自定义排序
// 先根据学生名字长度排序 前小后大
// 再根据学号排序 前小后大
Collections.sort(arrayList, (o1, o2) -> {
int i1 = o1.getName().length() - o2.getName().length();
int i2 = o1.getNo() - o2.getNo();
return i1 == 0 ? i2 : i1;
});
// [Student{name='lisi', no=4}, Student{name='wangwu', no=4},
// Student{name='wangwu', no=5}, Student{name='zhangsan', no=3}]
System.out.println(arrayList); // 按照自定义顺序输出
复制集合
// 必须有这一步操作,否则报异常
List<Student> arrayListCopy = new ArrayList<>();
for (Student student : arrayList) {
arrayListCopy.add(null);
}
Collections.copy(arrayListCopy,arrayList);
System.out.println(arrayListCopy);
List和Array转换
集合→数组
// 集合->数组
// new Student[0] 中的0只作为一个创建数组的参数,传多少都可以
// 如果长度小于等于集合长度,无所谓,会直接复制集合
// 如果长度大于集合长度,多出来的位置用数据类型的默认值补充
Student[] array = list.toArray(new Student[0]);
// [Student{name='zhangsan', no=3}, Student{name='lisi', no=4},
// Student{name='wangwu', no=5}, Student{name='wangwu', no=4}]
System.out.println(Arrays.toString(array));
数组→集合
// 数组→集合
// 此时list2是一个受限集合,不能添加和删除
// [Student{name='zhangsan', no=3}, Student{name='lisi', no=4},
// Student{name='wangwu', no=5}, Student{name='wangwu', no=4}]
List<Student> list2 = Arrays.asList(array);
System.out.println(list2);
基本数据类型
数组→集合
int[] ingArray = {213,43543,54,342,76,43};
// 方式1 不推荐
List<int[]> intList = Arrays.asList(ingArray);
for (int[] ints : intList) {
System.out.println(Arrays.toString(ints));
}
// 方式2 推荐
// 尽量使用基本数据类型的包装类
Integer[] integerArray = {213,43543,54,342,76,43};
List<Integer> integerList = Arrays.asList(integerArray);
System.out.println(integerList);
10章 IO流
概述
存储和读取数据的解决方案
I:input
O:output
流:像水流一样进行数据传输
以内存为基准
Java中I/O操作主要是指使用java.io包下的内容,进行输入、输出操作。 输入 也叫做读取 数据,输出 也叫做作写出 数据
File类
表示系统中的文件 或者是文件夹 的路径
注意:
File类只能对文件本身 进行操作 ,但是不能读写文件里面存储的数据
IO的分类
根据数据流向:
输入流、输出流
输入流:把数据从其他设备上读取到内存中的流。
输出流:把数据从内存中写出到其他设备上的流。
根据操作文件的类型:
字节流、字符流
字节流:以字节为单位,读写数据的流。
字符流:以字符为单位,读写数据的流。
字节流能操作所有类型的文件(视频、音频、word文档...)
字符流只能操作纯文本类型文件(通过记事本直接打开并能看懂的文件,如txt文件、md文件、xml文件)
顶级父类
这些都是抽象类
**输入流 **
输出流
字节流
InputStream
**OutputStream **
字符流
**Reader **
Write
字节流
FileOutPutStream
操作本地文件的字节输出流,可以把程序中的数据写到本地文件
基本操作
// 创建对象
OutputStream fileOutputStream = new
// 如果该路径下没有jjh01test.txt文件,会自动创建
FileOutputStream("C:\\Users\\Administrator\\Desktop\\ioTest\\jjh01test.txt");
// 写出数据
fileOutputStream.write(97);
// 关闭资源
fileOutputStream.close();
书写细节
创建对象
参数是字符串表示的文件路径或者是File对象
在保证父级路径存在的前提下,如果文件不存在就会创建新的文件
如果文件已经存在,构造方法会将文件中的内容(数据)清空
写出数据
虽然write方法传递的参数是整数,但写到文件中的内容是整数在ASCII上对应的字符
虽然参数为int类型四个字节,但是只会保留一个字节的信息写出
释放资源
3种写数据方式
方法名称
说明
void write(int b)
一次写一个字节数据
void write(byte[] b)
一次写一个字节数组数据
void write(byte[] b, int off, int len)
一次写一个字节数组的部分数据
// 写出数据
// 写一个字节
fileOutputStream.write(97);
// 写一个数组
fileOutputStream.write("zhangsan,lisi,wangwu,".getBytes());
// 写一个数组的部分数据
fileOutputStream.write("zhangsan,lisi,wangwu".getBytes(),0,8); // 从索引0开始,长度为8,所以是 zhangsan
实现换行写和续写
换行写:继续写出一个换行符就可以
Windows:\r\n
Linux:\n
Max:\r
在windows系统当中,java对回车换红进行了优化,虽然完整的是\r\n,但是如果只写\r或者\n,也会实现换行,因为java在底层会补全,但是建议还是写全
// 创建对象
String upload = "C:\\Users\\Lenovo\\Desktop\\test\\test01.txt";
OutputStream fileOutputStream = new FileOutputStream(upload);
// 写出数据
// 换行符号字节数组
byte[] wrapBytes = "\r\n".getBytes();
fileOutputStream.write("zhangsan".getBytes());
fileOutputStream.write(wrapBytes);
fileOutputStream.write("lisi".getBytes());
fileOutputStream.write(wrapBytes);
fileOutputStream.write("wangwu".getBytes());
// 关闭资源
fileOutputStream.close();
续写:调用构造方法 public FileOutputStream(String name, boolean append) boolean append
默认时false,表示覆盖写(不保留文件之前的数据内容)
true,保留之前的文件内容,续写
操作本地文件的字节输入流,可以把本地文件中的数据读取到程序中来
实现步骤
创建对象
读取数据
关闭资源
// 创建对象
// 如果文件不存在就会报错
String route = "C:\\Users\\Lenovo\\Desktop\\test\\test01.txt";
InputStream fileInputStream = new FileInputStream(route);
// 读取数据
// test01.txt中的内容是 abc
// read() 是一个字节一个字节的读,如果读取不到数据后,返回-1
int i1 = fileInputStream.read();
System.out.println((char)i1);
int i2 = fileInputStream.read();
System.out.println((char)i2);
int i3 = fileInputStream.read();
System.out.println((char)i3);
int i4 = fileInputStream.read();
System.out.println(i4); // 读取不到数据,返回-1
// 关闭资源
fileInputStream.close();
结果:
a
b
c
-1
实现细节
创建对象
读取数据
一次读一个字节,读出来的数据是再ASCII上对应的数字
读到文件末尾,read()方法返回-1
关闭资源
// 创建对象
String route = "C:\\Users\\Lenovo\\Desktop\\test\\test01.txt";
InputStream fileInputStream = new FileInputStream(route);
// 读取数据 循环读取
int b;
while((b = fileInputStream.read()) != -1){
System.out.print((char)b);
}
// 关闭资源
fileInputStream.close();
小练习:
编写代码实现图片复制
注意点:不要将本地文件的视频全部读取到内存中,然后再全部写出到本地文件中,而是要边读边写
// 创建对象
String route = "C:\\Users\\Lenovo\\Desktop\\test\\a.webp";
InputStream fileInputStream = new FileInputStream(route);
OutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Lenovo\\Desktop\\test\\copy.webp");
// 读取数据、写入数据
// 核心思想 边读边写
int b;
while((b = fileInputStream.read()) != -1){
fileOutputStream.write(b);
}
System.out.println("拷贝完成");
// 释放资源 原则:先打开的通道最后再关闭
fileOutputStream.close();
fileInputStream.close();
读取数据方式
方法
说明
public int read()
一次只能读取一个字节
public int read(byte[] buffer)
一次读一个字节数组数据(一次读取多个字节),数组的长度一般使用1024的整数倍(1024*1024就是1M)
使用 public int read(byte[] buffer)方法 完成视频的拷贝
// 创建对象
String route = "C:\\Users\\Lenovo\\Desktop\\test\\a.MOV";
InputStream fileInputStream = new FileInputStream(route);
OutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Lenovo\\Desktop\\test\\copy.MOV");
// 读取数据、写入数据
// 核心思想 边读边写
byte[] bytes = new byte[1024*1024*5];
int len;
// fileInputStream.read(bytes)
// 一次读取多个字节数据,具体读多少和字节数组的长度有关
// 返回值len是读取到的字节个数
// 每次读取到的数据会从索引为0的位置填充len个字节的数据,len个字节后面的数据保留之前的数据
// 如果没有读到数据,返回-1
while((len = fileInputStream.read(bytes)) != -1){
fileOutputStream.write(bytes,0,len);
}
System.out.println("拷贝完成");
// 释放资源 原则:先打开的通道最后再关闭
fileOutputStream.close();
fileInputStream.close();
用异常处理IO流
原则:
将定义流的代码放到try结构块之外,初始值为null
将释放资源的代码放到finally块当中,保证不管有无异常一定会释放资源
// 定义流,初始值为null
InputStream fileInputStream = null;
OutputStream fileOutputStream = null;
try {
// 创建对象
String route = "C:\\Users\\Lenovo\\Desktop\\test\\a.MOV";
fileInputStream = new FileInputStream(route);
fileOutputStream = new FileOutputStream("C:\\Users\\Lenovo\\Desktop\\test\\copy.MOV");
// 读取数据、写入数据
byte[] bytes = new byte[1024*1024*5];
int len;
while((len = fileInputStream.read(bytes)) != -1){
fileOutputStream.write(bytes,0,len);
}
System.out.println("拷贝完成");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 对流进行非null判断
if(fileOutputStream != null){
// 释放资源 原则:先打开的通道最后再关闭
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
简化方案
上述释放资源代码太过繁琐,现有简化方案,简单的方式实现流资源用完自动释放
jdk7和jdk9都有相应的简化方案
格式上都是通过try后加小括号的形式实现的简化(只有实现了AutoCloseable接口的类,才能在小括号中创建对象)
jdk7方案
// 不是定义流,而是真正使用new关键字创建流
try(创建流对象1;创建流对象2){
// 可能出现异常的代码
} catch(异常类名 变量名){
// 异常处理代码
}
String targetRoute = "C:\\Users\\Lenovo\\Desktop\\test\\copy.MOV";
// 在小括号中创建流对象
try(InputStream fileInputStream = new FileInputStream(route);
OutputStream fileOutputStream= new FileOutputStream(targetRoute)) {
String route = "C:\\Users\\Lenovo\\Desktop\\test\\a.MOV";
// 读取数据、写入数据
byte[] bytes = new byte[1024 * 1024 * 5];
int len;
while ((len = fileInputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
System.out.println("拷贝完成");
} catch (IOException e) {
e.printStackTrace();
}
jdk9方案
// 不是定义流,而是真正使用new关键字创建流
创建流对象1;
创建流对象2;
tyr(流1变量名;流2变量名){
// 可能出现异常的代码
} catch(异常类名 变量名){
// 异常处理代码
}
public static void jianhaujdk7() throws FileNotFoundException {
// 创建对象
String targetRoute = "C:\\Users\\Lenovo\\Desktop\\test\\copy.MOV";
InputStream fileInputStream = new FileInputStream(route);
OutputStream fileOutputStream= new FileOutputStream(targetRoute)
// 在小括号中写流的变量名
// 注意,此处还有异常,抛出到方法即可
try(fileInputStream,fileOutputStream) {
String route = "C:\\Users\\Lenovo\\Desktop\\test\\a.MOV";
// 读取数据、写入数据
byte[] bytes = new byte[1024 * 1024 * 5];
int len;
while ((len = fileInputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
System.out.println("拷贝完成");
} catch (IOException e) {
e.printStackTrace();
}
}
字符集
计算机的存储规则
在计算机中,任意数据都是以二进制的形式来存储的
字节是计算机最小的存储单元
存储英文只需要一个字节
源码、反码、补码
计算机中存储的二进制都是以补码的方式进行存储的
正整数的源码、反码、补码就是该正整数原本的二进制形式
负整数存储使用补码的形式,所以反码、补码针对负整数才有意义
小知识:
最高位的1代表是负数,0是正数
针对true和false来说 1是true 0是flase
举例:
// +3 的源码、反码、补码
0 000 0011
// -3 的源码、反码、补码
1 000 0011 // 源码:3的源码,最高符号位取反
1 111 1100 // 反码:除符号位之外,其他位取反(1变0 0变1)
1 111 1101 // 补码:在反码的基础上再加1(二进制 逢二进一)
// 负整数二进制转换位十进制
1 111 1101 // -3的二进制
// 计算,最高位是1,所以取-1
-1*2(8) + 1*2(7) + 1*2(6) + 1*2(5) + 1*2(4) + 1*2(3) +1*2(2) +0*2(1) +1*2(0)=-3
ASCII编码集
只有128(从0到127)个字符
一个字节的取值范围是(-128~127),一共有256个数,所以能够完全包含ASCII编码集
GBK字符集
介绍
GB2312字符集
1980年发布,1981年5月1日实施的简体中文汉字编码国家标准,收录7445个图形字符,其中包括6763个简体汉字
BIG5字符集
台湾地区繁体中文标准字符集,共收录13053个中文字,1984年实施
GBK字符集
2000年3月17日发布,收录21003个汉字。包含国家标准GB13000-1中的全部中日韩文字,和BIG5编码中的汉字,和ASCII字符集
windows系统默认使用的就是GBK,但是系统显示的是ANSI(包含简体中文GBK,繁体BIG5,韩文EUC-KR,日文Shift-JS)
Unicode字符集 (万国码)
国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本交换信息
英文字符存储
英文字符的存储原理和ASCII编码集一样
中文字符存储
一个中文字符占用两个字节(16个比特位),两个字节能表示65535个字符,所以表示GBK包含的字符足够了。
高位字节二进制一定以1开头,转成10进制后是个负数
GBK的英文字符存储以0开头
GBK的中文字符存储以1开头
通过这一点能够区分时英文字符还是中文字符
10111010 10111010二进制
使用GBK字符集,转换成整数时47802
如果按照计算规则的话,就是 -17734 最高符号位是1,所以是负数
Unicode字符集
万国码(万:代表大多数国家)
编码规则
UTF-16:用2~4个字节保存 英文字符需要占用2个字节
UTF-32:固定使用四个字节保存
UTF-8:用1~4个字节保存,英文字符使用1个字节,简体中文使用3个字节保存
ASCII 字符集中的英文字母,使用1个字节,形式:0xxx xxxx
希腊文、希伯来文,使用2个字节
中、日、韩使用3个字节,形式:1110xxxxx 10xxxxxx 10xxxxxx
其他国家使用4个字节
英文字符存储
中文字符存储
总结
英文字符,占用一个字节,首位是0,是一个正数
简体中文,占用三个字节,首位是1,首个字节是一个负数(其实三个字节都是复数)
乱码
出现乱码原因
读取数据的时候未读完整个汉字
编码和解码使用的方式不统一
解决乱码
不要用字节流读取文本文件
编码和解码时,用同一种字符集,同一种编码方式
拓展:
为什么字节流读取数据会乱码,但是拷贝文本文件数据不会乱码
因为虽然数据是一个字节一个字节的写入到目的地,但是最终写到目的地的字节是完整的,不会丢失。保证了字节写入的完整性,另外目的地使用的字符集和源数据的字符集一样,所以并不会出现乱码
编解码方法
编码
String str = "ai你呦";
// 使用UTF-8编码,英文1个字节,中文3个字节
byte[] utf8Bytes = str.getBytes();
System.out.println(Arrays.toString(utf8Bytes)); // [97, 105, -28, -67, -96, -27, -111, -90]
// 使用GBK编码,英文1个字节,中文2个字节
byte[] gbkBytes = str.getBytes("GBK");
System.out.println(Arrays.toString(gbkBytes)); // [97, 105, -60, -29, -33, -49]
解码
// 使用UTF-8解码,英文1个字节,中文3个字节
byte[] utf8Bytes = {97, 105, -28, -67, -96, -27, -111, -90}; // UTF-8编码得到
System.out.println(new String(utf8Bytes)); // 默认使用UTF-8解码 ai你呦
// 使用GBK解码,英文1个字节,中文2个字节
byte[] gbkBytes = {97, 105, -60, -29, -33, -49}; // GBK编码得到
System.out.println(new String(gbkBytes, "GBK")); // 默认使用GBK解码 ai你呦
System.out.println(new String(gbkBytes)); // 使用UTF-8解码乱码 ai����
字符流
当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。
字符流的底层其实就是**字节流 **
字符流 = 字节流 + 编码集
特点:
输入流:一次读一个字节,遇到中文时,一次读多个字节(读取字节的个数和字符集有关)
输出流:底层会把数据按照指定的编码方式进行编码,变成字节再写道文件中
使用场景:
FileReader
基本操作
创建对象
方法
描述
FileReader(File file)
创建一个新的 FileReader ,给定要读取的File对象
FileReader(String fileName)
创建一个新的 FileReader ,给定要读取的文件的名称。
读取数据
按字节进行读取,遇到中文,一次读取多个字节,读取后解码,返回一个整数
读到文件末尾,read方法返回-1
方法
描述
public int read()
读取数据,读到末尾返回-1
public int read(char[] buffer)
读取多个数据,读到末尾返回-1
关闭资源
方法
描述
public int close()
释放资源/关流
read()方法读取
// 文件路径
String fileRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test02.txt";
// 创建对象
Reader fileReader = new FileReader(fileRoute);
// 读取数据
// read()方法细节
// 1.默认一个字节一个字节读取,遇到中文,读取多个字节
// 2.在读取之后,将读取到的字节进行解码并转换位十进制
// 最终这个十进制作为返回值
// 这个十进制数据也表示在字符集上的数字
// 英文:文件里面的二进制数据 01100001 read方法进行读取,并转成十进制97,然后返回
// 中文:文件里面的二进制数据 11100110 10110001 10001001,转成十进制27721,然后返回
int ch;
while((ch = fileReader.read()) != -1){
// 20208228252282331505209863837621435652922510536744236822615934028339832015412290
System.out.print(ch);
// 仰天大笑出门去,我辈岂是蓬蒿人。
System.out.print((char)ch);
}
// 关闭资源
fileReader.close();
read(char[] buffer)方法读取
// 文件路径
String fileRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test02.txt";
// 创建对象
Reader fileReader = new FileReader(fileRoute);
// 定义数组,每次读两个字符的字节,英1中3
char[] chars = new char[2];
int len;
// read(chars) 细节:
// 1.读取数据、解码、强转三步合并,将强转之后的字符放进数组chars当中
// 2.相当于 空参的read()方法 + 强制类型转换
while((len = fileReader.read(chars)) != -1){
System.out.print(new String(chars,0,len)); // 仰天大笑出门去,我辈岂是蓬蒿人。
}
// 关闭资源
fileReader.close();
FileWriter
基本操作
创建对象
在父级目录存在的情况下,文件不存在则创建
如果文件已经存在,则会清空文件,如果不想清空,需要打开append续写开关
方法
描述
public FileWriter(String fileName)
创建字符输出流关联本地文件
public FileWriter(String fileName, boolean append)
append 续写,默认false,不续写
public FileWriter(File file)
创建字符输出流关联本地文件
public FileWriter(File file, boolean append)
append 续写,默认false,不续写
写出数据
如果writr传递的参数是整数,但是实际上写道本地文件中的是整数在字符集上对应的字符
方法
描述
void write(int c)
写入单个字符
void write(char[] cbuf)
写入字符数组
abstract void write(char[] cbuf, int off, int len)
写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
void write(String str)
写入字符串
void write(String str, int off, int len)
写入字符串的某一部分,off字符串的开始索引,len写的字符个数
关闭资源
方法
描述
void flush()
刷新该流的缓冲
void close()
关闭此流,但要先刷新它
// 文件路径
String fileRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test02.txt";
//创建对象
Writer fileWriter = new FileWriter(fileRoute);
// 写出数据
// 根据字符集进行编码,将编码后的字节存储到本地文件中
fileWriter.write("仰天大笑出门去,我辈岂是蓬蒿人。");
// 关闭资源
fileWriter.close();
FileReader源码分析
创建字符输入流对象时
关联文件,建立内存和本地文件通道,并在内存中创建一个长度为8192个字节数组的缓冲区
读取数据
判断缓冲区是否有数据可以读取
缓冲区如果没有数据
就从文件中获取数据,装到缓冲区,每次尽可能装满缓冲区,如果文件中也没有数据,返回-1
缓冲区有数据,就从缓冲区读取数据
如果使用的空参read方法,英文读取1个字节,中文读取多个字节,并把字节解码转成十进制返回
如果使用带参的read方法,就把读取字节,解码,强转合并,强转之后的字符放在数组当中
FileWriter源码分析
创建字符输入流对象时
关联文件,建立内存和本地文件通道,并在内存中创建一个长度为8192个字节数组的缓冲区
写出数据时
先将数据写入到缓冲区中(字节数组),英文字符一个字节,中文字符三个字节
数据真正写入到文件中的时机
当缓冲区存满之后(8192个字节全部都写入了数据),自动写出到本地文件
手动调用flush()方法,不管缓冲区数据有多少,都写入到本地文件中
执行close()方法,先将缓冲区的数据写出到文件中,然后再关闭流通道
flush和close方法
相同点:
不同点:
flush方法执行完之后,还可以继续向缓冲区当中写入数据
close方法执行后,不能再向缓冲区写入数据,关闭流通道
字节流和字符流使用场景
字节流
字符流
练习
拷贝文件夹
public static void main(String[] args) throws IOException {
// 拷贝需要源文件和目标文件
File src = new File("C:\\Users\\Lenovo\\Desktop\\test");
File target = new File("C:\\Users\\Lenovo\\Desktop\\copy");
copyFile(src,target);
}
public static void copyFile(File src, File target) throws IOException {
// 如果目标文件不存在,则创建
target.mkdirs();
// 进入数据源
File[] files = src.listFiles();
// 遍历文件数组,拷贝
for (File file : files) {
// 如果时文件,进行拷贝
if(file.isFile()){
InputStream fileInputStream = new FileInputStream(src);
OutputStream fileOutputStream = new FileOutputStream(new File(target,file.getName()));
byte[] bytes = new byte[1024];
int len;
while((len = fileInputStream.read(bytes)) != -1){
fileOutputStream.write(bytes,0,len);
}
fileOutputStream.close();
fileInputStream.close();
} else {
// 如果是文件夹,递归
copyFile(file,new File(target,file.getName()));
}
}
}
文件的加密和解密
public static void main(String[] args) throws IOException {
// 拷贝需要源文件和目标文件
// 源文件内容:仰天大笑出门去,我辈岂是蓬蒿人。
File src = new File("C:\\Users\\Lenovo\\Desktop\\test\\test02.txt");
// 加密完之后加密文件内容:婀茬Λ绂ュ畵鐓鸽暘鐚鬼編鋳撽紛绨€錃懏陳芥父醾€
File target = new File("C:\\Users\\Lenovo\\Desktop\\test\\test03.txt");
// 加密
jiaMiAndJieMi(src,target);
// 解密
jiaMiAndJieMi(target,src);
}
public static void jiaMiAndJieMi(File src,File target) throws IOException {
InputStream fileInputStream = new FileInputStream(src);
OutputStream fileOutputStream = new FileOutputStream(target);
int i;
while((i=fileInputStream.read()) != -1){
// 文件中的每个字节通过异或操作进行加密
int i1 = i ^ 2;
fileOutputStream.write(i1);
}
fileOutputStream.close();
fileInputStream.close();
}
缓冲流
缓冲流的基本原理:
创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
字节缓冲流
底层自带了长度为8192的字节数组 ,的缓冲区提高性能
创建对象
构造方法
描述
public BufferedInputStream(InputStream in)
把基本流包装成高级流,提高读取数据的性能
public BufferedOutputStream(OutputStream out)
把基本流包装成高级流,提高写出数据的性能
基本操作
一次读取一个字节
先一次性读取尽可能多的字节放进缓冲区(缓冲区大小默认8192),再从缓冲区一个字节一个字节的读取
String fileReadRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test01.txt";
String fileWriteRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test02.txt";
// 创建对象
// 默认的缓冲区大小是8192
InputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(fileReadRoute));
OutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(fileWriteRoute));
// 读取、写出数据
int i;
while((i = bufferedInputStream.read()) != -1){
bufferedOutputStream.write(i);
}
// 关闭资源
// 只关闭缓冲流通道即可,因为关闭缓冲流通道时,构建缓冲流的字节流也会进行关闭
bufferedOutputStream.close();
bufferedInputStream.close();
一次读取多个字节
先一次性读取尽可能多的字节放进缓冲区(缓冲区大小默认8192),再从缓冲区拿多个字节放进定义的数组
String fileReadRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test01.txt";
String fileWriteRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test02.txt";
// 创建对象
// 默认的缓冲区大小是8192
InputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(fileReadRoute));
OutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(fileWriteRoute));
// 读取、写出数据
int len;
byte[] bytes = new byte[1024];
while ((len = bufferedInputStream.read(bytes)) != -1) {
bufferedOutputStream.write(bytes, 0 , len);
}
// 关闭资源
// 只关闭缓冲流通道即可,因为关闭缓冲流通道时,构建缓冲流的字节流也会进行关闭
bufferedOutputStream.close();
bufferedInputStream.close();
底层原理
如果使用 int byte,数据在字节缓冲输入流和字节缓冲输出流之间一个字节一个字节的进行交接。
使用数组,是每次进行多个字节的交接
字符缓冲流
缓冲区是长度为8192的字符数组 ,在java中,一个字符占用两个字节
因为字符缓冲流的基本流是字符流,字符流已经有自带的缓冲区了,所以性能提高的并不明显,但是有些方法非常好用,在工作当中经常使用
创建对象
构造方法
描述
public BufferedReader(Reader in)
把基本字符输入流变成高级字符输入流
public BufferedWriter(Writer out)
把基本字符输出流变成高级字符输出流
读取、写出操作的特有方法
方法
描述
字符缓冲输入流 public String readLine()
读取一行数据,如果没有数据可读了,会返回null
字符缓冲输出流 public void newLine()
写一行行分隔符,由系统属性定义符号
基本操作
String fileReadRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test01.txt";
String fileWriteRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test02.txt";
// 创建对象
BufferedReader bufferedReader = new BufferedReader(new FileReader(fileReadRoute));
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(fileWriteRoute));
// 读取、写出数据
// 读取操作,先将数据尽可能多的读取到缓冲区
// 写出数据,先将数据写出到缓冲区,在缓冲区满时、缓冲区刷新时、关闭流时写出到本地文件中
String str;
// 使用readLine方法读数据时,一次读取一整行,遇到回车换行结束
// 但是不会将回车换行读取到内存当中
while((str = bufferedReader.readLine()) != null){
bufferedWriter.write(str);
bufferedWriter.newLine();
}
// 关闭资源
// 只关闭缓冲流通道即可,因为关闭缓冲流通道时,构建缓冲流的基本流也会进行关闭
bufferedWriter.close();
bufferedReader.close();
转换流
字符流和字节流之间的桥梁
字节输入流可以转换为字符输入流 InputStreamReader
字符输出流可以转换为字节输出流 OutputStreamWriter
示例1
手动创建一个GBK文件,把文件中的中文读取到内存中,不能出现乱码
/**
* 利用转换流按照指定字符编码读取
*/
// test03.txt文件是通过GBK进行编码的
String fileRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test03.txt";
// 如果没有指定编码格式,使用平台默认的编码方式,idea使用UTF-8
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(fileRoute));
int read = inputStreamReader.read();
// � 出现乱码,因为文件是通过GBK进行编码,而读取文件使用的是UTF-8进行解码
System.out.println((char)read); // �
// 指定编码格式,读取文件时,使用指定的GBK解码
// 这种方式在JDK11被淘汰了(了解即可)
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(fileRoute), "GBK");
int i;
while((i=inputStreamReader.read()) != -1){
System.out.print((char)i);
}
// 关闭资源
inputStreamReader.close();
// 替代方案,在JDK11中才被使用
// 创建对象
FileReader fileReader = new FileReader(fileRoute, Charset.forName("GBK"));
// 读取数据
int ch;
while((ch=inputStreamReader.read()) != -1){
System.out.print((char)i);
}
// 关闭资源
fileReader.close();
把一段中文按照GBK的方式写到本地文件
/**
* 利用转换流按照指定字符编码写出数据到本地文件
*/
// test03.txt文件是通过GBK进行编码的
String fileRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test04.txt";
// 创建对象
// 此方式在JDK11中被淘汰
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(fileRoute),"GBK");
// 写出数据
// 定义数据内容
String content = "你若夺我王冠,我必先毁你权杖,你若占我心房,我必先将你碎尸万段。";
outputStreamWriter.write(content);
// 关闭资源
outputStreamWriter.close();
// 替代方案(JDK11)
// 创建对象
FileWriter fileWriter = new FileWriter(fileRoute, Charset.forName("GBK"));
// 写出数据
// 定义数据内容
String content = "你若夺我王冠,我必先毁你权杖,你若占我心房,我必先将你碎尸万段。";
outputStreamWriter.write(content);
// 关闭资源
outputStreamWriter.close();
将本地文件中的GBK文件,转成UTF-8
通过InputStreamReader和OutputStreamWriter结合的方式,以文件拷贝的形式实现
先将GBK文件通过InputStreamReader指定GBK字符集进行解码,将文件数据读取到内存中
再将读取到的数据通过OutputStreamWriter指定UTF-8字符集进行编码,将数据写到文本文件中
示例2
利用字节流读取文件中的数据,每次读取一整行,而且不能出现乱码
/**
* 利用字节流读取文件中的数据,每次读取一整行,而且不能出现乱码
* 1.字节流在读取文本文件时是会出现乱码的,但是字符流能够搞定
* 2.但是字符流不能读取一整行方法,字符缓冲流能够搞定
*/
// test03.txt文件是通过GBK进行编码的
String fileRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test03.txt";
// 创建对象
// 字节流
FileInputStream fileInputStream = new FileInputStream(fileRoute);
// 字节流→字符流(转换流也是一种字符流),使用指定的字符集解码,解决乱码问题
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream,"GBK");
// 字符流→字符缓冲流,解决不能读取一整行问题
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
// 读取数据
String line;
while((line = bufferedReader.readLine()) != null){
System.out.println(line);
}
// 关闭资源
bufferedReader.close();
序列化流
序列化流:ObjectOutputStream
反序列化流:ObjectInputStream
序列化流
又叫对象操作输出流
可以把java中的对象写到本地文件当中,前提是java类必须实现Serializable接口
创建对象
构造方法
描述
public ObjectOutputStream(OutputStream out)
把基本流包装成高级流
写出方法
方法
描述
public final void writeObject (Object obj)
将对象序列化(写出)到文件中
public static void main(String[] args) throws IOException {
// 创建学生对象
Student zhangsan = new Student("zhangsan", 23);
// 创建序列化流/对象操作输出流对象
String fielRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test05.txt";
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(fielRoute));
// 写出操作
objectOutputStream.writeObject(zhangsan);
// 关闭资源
objectOutputStream.close();
}
/**
* Serializable接口里面没有抽象方法,是标记型接口
* 一旦实现了这个接口,那么就表示当前类可以被序列化
*/
class Student implements Serializable {
private String name;
private int age;
public Student() {
}
public Student(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 "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
反序列化流
又叫对象操作输入流
将本地文件中的java对象写到内存当中
创建对象
构造方法
描述
public ObjectInputStream(InputStream in)
把基本流包装成高级流
读取方法
方法
描述
public final Object readObject ()
把序列化到本地文件中的对象,读取到程序当中来
// 文件路径
String fileRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test05.txt";
// 创建反序列化流对象
FileInputStream fileInputStream = new FileInputStream(fileRoute);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
// 读取操作
Object object = objectInputStream.readObject();
System.out.println(object);
// 关闭资源
objectInputStream.close();
细节
固定版本号
实现序列化接口时,保证序列号不发生变化,避免对象反序列失败
class Student implements Serializable {
/**
* private:版本号私有,不让外界使用,无set和get方法
* static:表示这个类的所有的对象都共享同一个版本号
* final:版本号的值永远都不会发生变化
* long:在计算版本号时,计算出来的版本号的值比较长,int取值范围时不够的
* serialVersionUID:版本号变量名,固定的名称(必须使用这个名),不能自定义命名
*/
private static final long serialVersionUID = 1L;
private String name;
// transient:不会把当前的属性序列化到本地文件当中
private transient int age;
}
细节汇总
使用序列化将对象写到文件时,必须实现Serializable接口,否则会出现序列化异常
通过序列化流写到文件中的对象数据是不能修改的,一旦修改无法在读取回来
实现序列化接口时,固定序列号,避免对象反序列失败
如果类中的某个属性不想被序列化,使用transient关键字
练习
从本地文件中读取(反序列化)多个对象:
反序列化流每次只能读取一个对象
当文件中没有对象可读时,不会返回-1,也不会返回null值,而是程序会出现异常
所以习惯上就是在序列化对象是,把多个对象放在集合当中,然后序列化集合的对象
在反序列化时,读取一个集合的对象即可
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 文件路径
String fileRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test05.txt";
// 创建序列化流的对象
FileOutputStream fileOutputStream = new FileOutputStream(fileRoute);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
// 创建集合对象
ArrayList<Student> students = new ArrayList<>();
Student zhangsan = new Student("zhangsan", 23);
Student lisi = new Student("lisi", 24);
Student wangwu = new Student("wangwu", 25);
Student zhaoliu = new Student("zhaoliu", 26);
Student qianqi = new Student("qianqi", 27);
students.add(zhangsan);
students.add(lisi);
students.add(wangwu);
students.add(zhaoliu);
students.add(qianqi);
// 写出集合对象
objectOutputStream.writeObject(students);
// 关闭资源
objectOutputStream.close();
// 创建反序列化流对象
FileInputStream fileInputStream = new FileInputStream(fileRoute);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
// 读取操作
List studentList = (ArrayList<Student>)objectInputStream.readObject();
System.out.println(studentList);
// 关闭资源
objectInputStream.close();
}
/**
* Serializable接口里面没有抽象方法,是标记型接口
* 一旦实现了这个接口,那么就表示当前类可以被序列化
*/
class Student implements Serializable {
/**
* private:版本号私有,不让外界使用,无set和get方法
* static:表示这个类的所有的对象都共享同一个版本号
* final:版本号的值永远都不会发生变化
* long:在计算版本号时,计算出来的版本号的值比较长,int取值范围时不够的
* serialVersionUID:版本号变量名,固定的名称(必须使用这个名),不能自定义命名
*/
private static final long serialVersionUID = 4977938270082527813L;
private String name;
// transient:不会把当前的属性序列化到本地文件当中
private transient int age;
public Student() {
}
public Student(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 "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
解压缩流/压缩流
解压缩流(ZipInputStream)→输入流 负责把压缩包中的文件和文件夹解压出来
压缩流(ZipOutputStream)→输出流 负责压缩文件或者文件夹
解压缩流
zip文件目录结构
// 压缩包文件路径
String fileRoute = "C:\\Users\\Lenovo\\Desktop\\test\\test06.zip";
// 目的地目录路径(要把压缩包中的文件解压到哪里)
String targetRoute = "C:\\Users\\Lenovo\\Desktop\\test\\jjh";
// 解压的本质:把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地当中
// 创建对象:创建一个解压缩流对象,用来读取压缩包中的数据
ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(fileRoute));
// 读取数据:按照层级关系读取压缩包中的目录或者文件
ZipEntry zipEntry;
while((zipEntry=zipInputStream.getNextEntry()) != null){
System.out.println(zipEntry); // 第一次会读取压缩包中首个文件夹中的第一个文件/文件夹
if(zipEntry.isDirectory()){
// 如果是文件夹,需要在目的地处创建一个同样的文件夹
File file = new File(targetRoute, zipEntry.toString());
file.mkdirs();
} else {
// 如果是文件,需要读取压缩包中的文件,并把他存放到目的地的文件夹中(按照层级目录进行存放)
File file = new File(targetRoute, zipEntry.toString());
// 如果zipEntry父级文件夹不在,先创建父级文件夹
File parentFile = new File(file.getParent());
if(!parentFile.exists()){
parentFile.mkdirs();
}
FileOutputStream fileOutputStream = new FileOutputStream(file);
// 使用缓冲流,提高写出数据的效率
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
// 读取文件数据并写出文件数据
int i;
while((i=zipInputStream.read()) != -1){
bufferedOutputStream.write(i);
}
// 关闭资源
bufferedOutputStream.close();
}
// 表示在压缩包中的一个文件已经处理完毕
zipInputStream.closeEntry();
}
// 关闭资源
zipInputStream.close();
// 打印结果:
test06/aa/
test06/aa/test01.txt
test06/aa/test02.txt
test06/aa/test03.txt
test06/test04.txt
test06/test05.txt
test06/
解压缩流
将一个文件夹压缩成.zip文件
public static void main(String[] args) throws IOException {
// 创建File对象表示要压缩的文件夹
File fileRoute = new File("C:\\Users\\Lenovo\\Desktop\\test\\test06");
// 创建File对象表示压缩包在哪里
File parentFile = fileRoute.getParentFile();
// 创建File对象表示压缩包的路径
File targetFile = new File(parentFile, fileRoute.getName() + ".zip");
// 创建压缩流对象,用来将原文件夹的数据按照层级关系写出到目标压缩包中
FileOutputStream fileOutputStream = new FileOutputStream(targetFile);
ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
// 进行压缩
toZip(fileRoute,zipOutputStream,fileRoute.getName());
// 关闭资源
zipOutputStream.close();
}
/**
*
* @param fileRoute
* @param zipOutputStream
* @param name 代表压缩包中的目录结构,相当于他的根目录就是压缩包
* @throws IOException
*/
static void toZip(File fileRoute,ZipOutputStream zipOutputStream,String name) throws IOException {
// 遍历源文件夹中的数据,按照层级关系写书到压缩包中
File[] files = fileRoute.listFiles();
for (File file : files) {
if(file.isFile()){
// 如果file是文件,将文件变成ZipEntry对象,写出到压缩包中
// ZipEntry是压缩包中的数据结构
// ZipEntry可以创建多级目录的文件,
ZipEntry zipEntry = new ZipEntry(name +"\\"+file.getName());
zipOutputStream.putNextEntry(zipEntry);
// 读取文件数据,写出到压缩包
FileInputStream fileInputStream = new FileInputStream(file);
int i;
while((i=fileInputStream.read()) !=-1){
zipOutputStream.write(i);
}
fileInputStream.close();
zipOutputStream.closeEntry();
} else {
// 如果是文件夹,则递归
toZip(file,zipOutputStream,name+"\\"+file.getName());
}
}
}
Commons-io工具包
apache开源基金组织提供的一组有关io操作的开源工具包
commons-io-2.11.0.jar
使用方式:
1,新建lib文件夹
2,把第三方jar包粘贴到文件夹中
3,右键点击add as a library
FileUtils(文件操作工具类)
描述
static void copyFile(File srcFile, File destFile)
复制文件
static void copyDirectory(File srcDir, File destDir)
复制文件夹
static void copyDirectoryToDirectory(File srcDir, File destDir)
复制文件夹
static void deleteDirectory(File directory)
删除文件夹
static void cleanDirectory(File directory)
清空文件夹
static String readFileToString(File file, Charset encoding)
读取文件中的数据变成成字符串
static void write(File file, CharSequence data, String encoding)
写出数据
IoUtils(Io流操作工具类)
描述
public static int copy(InputStream input, OutputStream output)
复制文件
public static int copyLarge(Reader input, Writer output)
复制大文件
public static String readLines(Reader input)
读取数据
public static void write(String data, OutputStream output)
写出数据
使用方式:
1,新建lib文件夹
2,把第三方jar包粘贴到文件夹中
3,右键点击add as a library
FileUtiles
描述
file
根据参数创建一个file对象
touch
根据参数创建文件
writeLines
把集合中的数据写出到文件中,覆盖模式
appendLines
把集合中的数据写出到文件中,续写模式
readLines
指定字符编码,把文件中的数据,读到集合中
readUtf8Lines
按照UTF-8的形式,把文件中的数据,读到集合中
copy
拷贝文件或者文件夹
11章 多线程
线程简介
程序,进程,线程
程序:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
进程: 而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的的单位
注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
核心概念理解
线程就是独立的执行路径
在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
main() 称之为主线程,为系统的入口,用于执行整个程序;
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的。
对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
程会带来额外的开销,如cpu调度时间,并发控制开销。
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程实现(重点)
Thread 、Runnable、Callable
三种创建方式
Thread
自定义线程类继承Thread类
重写run()方法,编写线程执行体
创建线程对象,调用start()方法启动线程
实现Runnable
定义MyRunnable类实现Runnable接口
实现run()方法,编写线程执行体
创建线程对象,调用start()方法启动线程
Thread和Runnable区别
Thread,一个线程一个对象,多线程用多个线程对象(new多次对象)
Runnable,可以是一个线程一个对象,多个线程用多个线程对象,也可以多个线程使用同一个线程对象
总结:Thread不建议使用,避免oop单继承局限性。Runnable推荐使用,灵活方便,方便多个线程使用一个对象。
实现 Callable接口 (了解即可 ) (小点代码)
实现Callable接口,需要返回值类型
重写call方法,需要抛出异常
创建目标对象
创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
提交执行:Future result1 = ser.submit(t1);
获取结果:boolean r1 = result1.get()
关闭服务:ser.shutdownNow();
静态代理模式(小点代码)
好处:
真实对象做不了的事情由代理对象做
真实对象专注做自己的事情
总结:通过Runnable接口实现多线程使用的就是静态代理模式
// 真实对象
public class RunnableTest implments Runnable{
@overide
void run(){
System.out.println("我爱你");
}
}
// Thread 静态代理对象,也实现了Runnable接口
Thread thread=new Thread(new Runnable());
// lambda表达式实现
Thread thread=new Thread(()->System.out.println("我爱你"));
lambda表达式(小点代码)
对于函数式接口,我们可以通过lambda表达式来创建该接口的对象
函数式接口:任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口
表达式简化:
// 正常方式
ILove love = (int a) -> {System.out.println("i love you")};
// 简化1:参数类型简化(如果多个参数,参数类型要么都加上,要么都不加)
ILove love = (a) -> {System.out.println("i love you")};
// 简化2:简化参数括号(只有一个参数,多个参数加括号)
ILove love = a -> {System.out.println("i love you")};
// 简化3:去掉花括号(lambda表达式只能由一行代码的情况下,才能去掉花括号)
ILove love = a -> System.out.println("i love you");
线程状态
线程状态
线程方法
停止线程
不推荐使用JDK提供的 stop()、destroy()方法。【已废弃】
推荐线程自己停止下来
建议使用一个标志位进行终止变量,当flag=false,则终止线程运行
public class ThreadStop implements Runnable {
// 定义标志位
boolean flag=true;
@Override
public void run() {
int i=0;
while(flag){
System.out.println(Thread.currentThread().getName()+"跑到了"+i++);
}
}
// 编写线程停止的方法
public void stop(){
this.flag=false;
System.out.println("线程停止");
}
public static void main(String[] args) {
ThreadStop threadStop = new ThreadStop();
new Thread(threadStop).start();
for (int i = 0; i < 500; i++) {
if(i==200){
threadStop.stop();
}
System.out.println(Thread.currentThread().getName()+"跑到了"+i);
}
}
}
结果:
main跑到了199
Thread-0跑到了50
线程停止
main跑到了200
线程休眠
sleep (时间) 指定当前线程阻塞的毫秒数
sleep存在异常InterruptedException
sleep时间达到后线程进入就绪状态;
sleep可以模拟网络延时,倒计时等。
每一个对象都有一个锁,sleep不会释放锁;
public class ThreadDormin {
// 模拟倒计时
static void dormin() throws InterruptedException {
int i=10;
while(true){
Thread.sleep(1000);
System.out.println(i--);
if(i==0){
break;
}
}
}
public static void main(String[] args) throws InterruptedException {
dormin();
}
}
线程礼让(yield方法)
让当前正在执行的线程暂停,但不阻塞
线程从运行状态转为就绪状态
让cpu重新调度,礼让不一定成功!看CPU心情
join
join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
可以想象成插队
线程状态观测
线程优先级
守护(daemon)线程
举例理解:上帝守护人类的寿命,人类就是用户线程,上帝的守护就是守护线程,用户线程结束也就结束了,不用等待守护线程。
线程分为用户线程和守护线程
虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕
如,后台记录操作日志,监控内存,垃圾回收等待..
线程同步(重点)
三大线程不安全案例(小点代码)
并发 : 同一个对象被多个线程同时操作
现实生活中,我们会遇到 ” 同一个资源 , 多个人都想使用 ” 的问题 , 比如,食堂排队打饭 , 每个人都想吃饭 , 最天然的解决办法就是 , 排队 . 一个个来
处理多线程问题时 , 多个线程访问同一个对象 , 并且某些线程还想修改这个对象 .这时候我们就需要线程同步 . 线程同步其实就是一种等待机制 , 多个需要同时访问此对象的线程进入这个对象的等待池 形成队列, 等待前面线程使用完毕 , 下一个线程再使用
队列和锁
队列(排队,一个一个来)+锁来保证线程安全性(线程同步)
线程同步
由于同一进程的多个线程共享同一块存储空间 , 在带来方便的同时,也带来了访问冲突问题 , 为了保证数据在方法中被访问时的正确性 , 在访问时加入锁机制synchronized , 当一个线程获得对象的排它锁 , 独占资源 , 其他线程必须等待 ,使用后释放锁即可 . 存在以下问题 :
一个线程持有锁会导致其他所有需要此锁的线程挂起
在多线程竞争下 , 加锁 , 释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题
如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置 , 引起性能问题(性能倒置)
同步方法(小点代码->买票)
由于我们可以通过 private 关键字来保证数据对象只能被方法访问 , 所以我们只需要针对方法提出一套机制 , 这套机制就是 synchronized 关键字 , 它包括两种用法 : synchronized 方法和synchronized 块
同步方法 : public synchronized void method(int args) {}
synchronized方法控制对 “对象” 的访问 , 每个对象对应一把锁 , 每个synchronized方法都必须获得调用该方法的对象的锁 才能执行 , 否则线程会阻塞 ,方法一旦执行 , 就独占该锁 , 直到该方法返回才释放锁 , 后面被阻塞的线程才能获得这个锁 , 继续执行.
缺陷:若将一个大的方法申明为synchronized 将会影响效率,方法里面需要修改的内容才需要锁,锁的太多 , 浪费资源
同步块(小点代码->银行取钱、ArrayList集合)
同步块 : synchronized (Obj ) { }
Obj 称之为同步监视器
Obj 可以是任何对象 , 但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是this , 就是这个对象本身 , 或者是 class[反射中讲解]
同步监视器的执行过程
第一个线程访问 , 锁定同步监视器 , 执行其中代码
第二个线程访问 , 发现同步监视器被锁定 , 无法访问
第一个线程访问完毕 , 解锁同步监视器
第二个线程访问, 发现同步监视器没有锁 , 然后锁定并访问
死锁(小点代码)
多个线程各自占有一些共享资源 , 并且互相等待其他线程占有的资源才能运行 , 而导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形 . 某一个同步块同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题
简单来说就是:多个线程互相抱着对方需要的资源,然后形成僵持
死锁的避免方法
产生死锁的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件 : 进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件 : 若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生
Lock(锁)(小点代码)
JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
synchronized 与 Lock 的对比
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
线程通信问题
生产者和消费者
Java提供了几个方法解决线程之间的通信问题
注意:均是Object类的方法 , 都只能在同步方法或者同步代码块中使用,否则会抛出异IllegalMonitorStateException
解决方式 1(小点代码)
并发协作模型 “ 生产者 / 消费者模式 ” --->管程法
生产者 : 负责生产数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;
消费者 : 负责处理数据的模块 (可能是方法 , 对象 , 线程 , 进程)
缓冲区 : 消费者不能直接使用生产者的数据 , 他们之间有个 “ 缓冲区
生产者将生产好的数据放入缓冲区 , 消费者从缓冲区拿出数据
解决方式 2
并发协作模型 “ 生产者 / 消费者模式 ” --->信号灯法
使用线程池
JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
void shutdown() :关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
package javatest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 测试线程池
public class TestPool {
public static void main(String[] args) {
// 1.创建服务,创建线程池
// 参数为线程池大小
ExecutorService service= Executors.newFixedThreadPool(10);
// 执行服务
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
// 关闭链接
service.shutdownNow();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
12章 注解
什么是注解
Annotation 是从JDK5.0开始引入的新技术
Annotation的作用
不是程序本身 , 可以对程序作出解释.(这一点和注释(comment)没什么区别)
可以被其他程序(比如:编译器等)读取
Annotation的格式
注解是以"@注释名"在代码中存在的
还可以添加一些参数值 , 例如:@SuppressWarnings(value="unchecked")
Annotation在哪里使用?
可以附加在package , class , method , field 等上面 , 相当于给他们添加了额外的辅助信息
我们可以通过反射机制实现对这些元数据的访问
内置注解
@Override
定义在 java.lang.Override 中
此注释只适用于修辞方法 ,
表示一个方法声明打算重写超类中的另一个方法声明
@Deprecated
定义在java.lang.Deprecated中 ,
此注释可以用于修辞方法 , 属性 , 类 。
表示不鼓励程序员使用这样的元素 , 通常是因为它很危险或者存在更好的选择
@SuppressWarnings
定义在java.lang.SuppressWarnings中,用来抑制编译时的警告信息
与前两个注释有所不同,你需要添加一个参数才能正确使用,这些参数都是已经定义好了的,我们选择性的使用
@SuppressWarnings("all")
@SuppressWarnings("unchecked")
@SuppressWarnings(value={"unchecked","deprecation"})
等等 .....
package com.annotation;
//所有类默认继承Object类
public class Test1 extends Object {
//@Override 表示方法重写
@Override
public String toString() {
return super.toString();
}
//方法过时了, 不建议使用 , 可能存在问题 , 并不是不能使用!
@Deprecated
public static void stop(){
System.out.println("测试 @Deprecated");
}
//@SuppressWarnings 抑制警告 , 可以传参数
//查看源码:发现 参数类型 和 参数名称 , 并不是方法!
@SuppressWarnings("all")
public void sw(){
List list = new ArrayList();
}
public static void main(String[] args) {
stop();
}
}
元注解
负责注解其他注解,被用来提供对其他annotation类型作说明
Java定义了4个标准的meta-annotation类型
@Target
用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
类或接口:ElementType.TYPE;
字段:ElementType.FIELD;
方法:ElementType.METHOD;
构造方法:ElementType.CONSTRUCTOR;
方法参数:ElementType.PARAMETER。
@Retention
表示需要在什么级别保存该注释信息(在什么地方仍然有效) , 用于描述注解的生命周期
SOURCE < CLASS < RUNTIME
RetentionPolicy.SOURCE:在源码时,注解有效
RetentionPolicy.CLASS:在源码和class时,注解有效
RetentionPolicy.RUNTIME:在 源码、class、运行时,注解都有效,我们通常使用这个
@Repeatable
定义Annotation是否可重复
// @Report注解被@Repeatable修饰后,可以在同一个地方重复使用
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
@Document
说明该注解将被包含在javadoc中
@Inherited
说明子类可以继承父类中的该注解,仅对类的继承有效,对interface的继承无效
@MyAnnotation01
public class Test01 {
@MyAnnotation01
public void test(){
}
}
// 定义一个注解
// 表示我们的注解可以用在那些地方,在这里定义为可以用在方法和类上
@Target(value = {ElementType.METHOD,ElementType.TYPE})
// 表示我们定义的注解在运行仍然有效
@Retention(value = RetentionPolicy.RUNTIME)
// 表示将我们的注解生成在JAVAdoc中
@Documented
// 表示子类可以继承父类的注解
@Inherited
@interface MyAnnotation01{
}
自定义注解
使用 @interface自定义注解, 自动继承了java.lang.annotation.Annotation接口
分析
@ interface用来声明一个注解 , 格式 : public @ interface 注解名
注解中的每一个方法实际上是声明了一个配置参数.
方法的名称就是参数的名称.
返回值类型就是参数的类型 ( 返回值只能是基本类型,Class , String , enum ).
可以通过default来声明参数的默认值
如果只有一个参数成员 , 一般参数名为value
注解元素必须要有值 , 我们定义注解元素时 , 经常使用空字符串,0作为默认值
public class Test02 {
// 可以显示的给name参数进行赋值,如果不写参数,使用默认值
// 如果没有默认值,参数必须要写
@MyAnnotation02(age = 18)
// 注解只有一个参数,并且参数名是value,就不用写参数名了
@MyAnnotation03("zhangsan")
public void test(){
}
}
@Target(value = {ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation02{
// 并不是一个方法,而是一个参数
// default 给参数设置一个默认值
String name() default "";
int age();
int id() default -1; // 如果默认值为-1,代表不存在
String[] schools() default {"清华大学","北京大学"};
}
@Target(value = {ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation03{
// 如果注解只有一个参数,建议使用 value 作为参数名
// 这样在使用注解时,不用写参数名
String value();
}
13章 反射
动态语言和静态语言
动态语言
运行时可以改变其结构的语言,通俗点说就是在运行时代码可以根据某些条件改变自身结构
Object-C、C#、JavaScript、PHP、Python等
静态语言
运行时结构不可变的语言就是静态语言。如Java、C、C++
Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制获得类似动态语言
的特性。
Java Reflection
Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信
息,并能直接操作任意对象的内部属性及方法。
Class c = Class.forName("java.lang.String")
加载完类之后,在堆内存的方法区中 就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以我
们形象的称之为:反射
一个类只有一个Class对象
当一个类被加载后,这个类的所有信息就会被封装到Class对象中
反射优点和缺点
优点
缺点
对性能有影响,使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于 直接执行相同的操作
反射相关的主要API
java.lang.Class : 代表一个类
java.lang.reflect.Method : 代表类的方法
java.lang.reflect.Field : 代表类的成员变量
java.lang.reflect.Constructor : 代表类的构造器
......
Class类
对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE 都为
其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个结构(class/interface/enum/annotation/primitive
type/void/[])的有关信息。
Class 本身也是一个类
Class 对象只能由系统建立对象
一个加载的类在 JVM 中只会有一个Class实例
一个Class对象对应的是一个加载到JVM中的一个.class文件
每个类的实例都会记得自己是由哪个 Class 实例所生成
通过Class可以完整地得到一个类中的所有被加载的结构
Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象
Class类的常用方法
方法名
功能说明
static ClassforName(String name)
返回指定类名name的Class对象
Object newInstance()
调用缺省构造函数,返回Class对象的一个实例
getName()
返回此Class对象所表示的实体(类,接口,数组类或void)的名称。
Class getSuperClass()
返回当前Class对象的父类的Class对象
Class[] getinterfaces()
获取当前Class对象的接口
ClassLoader getClassLoader()
返回该类的类加载器
Constructor[] getConstructors()
返回一个包含某些Constructor对象的数组
Method getMothed(String name,Class.. T)
返回一个Method对象,此对象的形参类型为paramType
Field[] getDeclaredFields()
返回Field对象的一个数组
获取Class类的实例
a)若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高。
Class clazz = Person.class;
b)已知某个类的实例,调用该实例的getClass()方法获取Class对象
Class clazz = person.getClass();
c)已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出
ClassNotFoundException
Class clazz = Class.forName("demo01.Student");
d)内置基本数据类型可以直接用类名.Type
e)还可以利用ClassLoader我们之后讲解
public class Test01 {
public static void main(String[] args) throws ClassNotFoundException {
Person person = new Student();
System.out.println(person.name); // 学生
// 方式1:通过对象获得
Class c1 = person.getClass();
// 方式2:通过类获得
Class c2 = Student.class;
// 方式3:通过Class的静态方法获得
Class c3 = Class.forName("com.jjh.reflection.Student");
System.out.println(c1.hashCode()); // 460141958
System.out.println(c2.hashCode()); // 460141958
System.out.println(c3.hashCode()); // 460141958
// 方式4:基本数据类型的包装类都有一个Type属性
Class c4 = Integer.TYPE;
// 获取父类类型对象
Class c5 = c1.getSuperclass();
System.out.println(c5); // class com.jjh.reflection.Person
}
}
class Person{
public String name;
}
class Student extends Person{
public Student() {
this.name = "学生";
}
}
class Teacher extends Person{
public Teacher() {
this.name = "老师";
}
}
哪些类型可以有Class对象
// 类
Class c1 = Object.class;
System.out.println(c1); // class java.lang.Object
// 接口
Class c2 = Comparable.class;
System.out.println(c2); // interface java.lang.Comparable
// 一维数组
Class c3 = String[].class;
System.out.println(c3); // class [Ljava.lang.String;
// 二维数组
Class c4 = int[][].class;
System.out.println(c4); // class [[I
// 注解
Class c5 = Override.class;
System.out.println(c5); // interface java.lang.Override
// 基本数据类型包装类
Class c6 = Integer.class;
System.out.println(c6); // class java.lang.Integer
// 枚举
Class c7 = ElementType.class;
System.out.println(c7); // class java.lang.annotation.ElementType
// void
Class c8 = void.class;
System.out.println(c8); // void
// Class类
Class c9 = Class.class;
System.out.println(c9); // class java.lang.Class
java内存分析
类的加载过程
当程序主动使用某个类,但是这个类还未被加载到内存中,则系统和会通过下面的三个步骤对该类进行初始化
初始化就是将类加载到内存中,并构造类信息,并不是创建对象的初始化
加载
将class文件字节码内容加载到内存中
将字节码等静态数据转化为方法区的运行时数据结构
生成一个代表这个类的java.lang.Class对象
链接
将Java类的二进制代码合并到JVM的运行状态之中的过程
验证:确保加载的类信息符合JVM规范,没有安全方面的问题
准备:为类变量(static)分配内存并设置默认初始值
这些内存都将在方法区中进行分配、
默认初始值:基本数据类型使用其默认的初始值,引用数据类型默认值为null
解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程
初始化
执行类构造器()方法的过程。
类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中 的语句合并产生的。
类构造器是构造类信息的,不是构造该类对象的构造器
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
虚拟机会保证一个类的构造器()方法在多线程环境中被正确加锁和同步。
public class Test03 {
public static void main(String[] args) {
A a = new A();
System.out.println(a.m); // 100
}
}
class A{
static {
System.out.println("A类静态代码块初始化");
}
public static int m = 100;
public A() {
System.out.println("A类无参构造方法初始化");
}
}
运行结果:
A类静态代码块初始化
A类无参构造方法初始化
100
分析:
类的初始化时机
类的主动引用(一定会发生类的初始化)
当虚拟机启动,先初始化main方法所在的类
new一个类的对象
调用类的静态成员(除了final常量)和静态方法
使用java.lang.reflect包的方法对类进行反射调用
当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
类的被动引用(不会发生类的初始化)
当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化
// Son是子类,Father是父类,m是Father的静态变量
Son.m
通过数组定义类引用,不会触发此类的初始化
// Son这个类不会发生初始化
Son[] sonArr = new Son[](10)
引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
// f是Son类中的静态常量,Son类不会发生初始化
Son.f
类加载器的作用
类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象
类加载器作用是用来把类(class)装载进内存的。JVM 规范定义了如下类型的类的加载器
java 的jar包结构
获取加载器
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取系统加载器的父类加载器→拓展类加载器
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent); // sun.misc.Launcher$ExtClassLoader@1b6d3586
// 获取拓展类加载器的父类加载器→引导类加载器(跟加载器)
ClassLoader parent1 = parent.getParent();
System.out.println(parent1); // null,根加载器无法直接获取
获取某个类的加载器
查看某个类是由那个类加载器加载的
public class Test04 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = Class.forName("com.jjh.reflection.Test04").getClassLoader();
// Test04这个自定义类是由系统加载器加载进来的
System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// Object 属于java的核心包中的类,是由引导类加载器进行加载的
ClassLoader classLoader1 = Class.forName("java.lang.Object").getClassLoader();
System.out.println(classLoader1); // null
}
}
系统类加载器的加载路径
查看系统类加载器可以加载哪些包(路径)下的类
// property,就是系统类加载器可以加载的类的路径
String property = System.getProperty("java.class.path");
System.out.println(property);
/** 这些包下的类都由系统类加载器进行加载
* D:\Java\jdk1.8.0_261\jre\lib\charsets.jar;
* D:\Java\jdk1.8.0_261\jre\lib\deploy.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\access-bridge-64.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\cldrdata.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\dnsns.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\jaccess.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\jfxrt.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\localedata.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\nashorn.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\sunec.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\sunjce_provider.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\sunmscapi.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\sunpkcs11.jar;
* D:\Java\jdk1.8.0_261\jre\lib\ext\zipfs.jar;
* D:\Java\jdk1.8.0_261\jre\lib\javaws.jar;
* D:\Java\jdk1.8.0_261\jre\lib\jce.jar;
* D:\Java\jdk1.8.0_261\jre\lib\jfr.jar;
* D:\Java\jdk1.8.0_261\jre\lib\jfxswt.jar;
* D:\Java\jdk1.8.0_261\jre\lib\jsse.jar;
* D:\Java\jdk1.8.0_261\jre\lib\management-agent.jar;
* D:\Java\jdk1.8.0_261\jre\lib\plugin.jar;
* D:\Java\jdk1.8.0_261\jre\lib\resources.jar;
* D:\Java\jdk1.8.0_261\jre\lib\rt.jar;
* D:\JetBrains\workspace\JavaSE\out\production\反射;
* D:\JetBrains\IntelliJ IDEA 2021.3.2\lib\idea_rt.jar
*/
双亲委派机制
举例:
如果我们自定义了一个 java.lang.String这样的类,我们知道jdk的核心库里面也有个同包同名的String类。在类加载的时候,首先系统类加载器发现了我们自定义的String类,然后拓展类加载器会接着找它负责的包下有没有同包同名的类。拓展类加载器找完,根加载器在它所负责的包下在找有没有同包同名的类,结果根加载器找到了核心库中的String类。这样就导致我们自定义的String类不能被加载,而是加载的根加载器负责的核心库中的java.lang.String类。
创建运行时类(Class)的对象
通过反射获取运行时类的完整结构
Field、Method、Constructor、Superclass、Interface、Annotation
实现的全部接口
所继承的父类
全部的构造器
全部的方法
全部的Field
注解
...
// 获取Person类的Class对象
Class c1 = Class.forName("com.jjh.reflection.Person");
// 获取Person类的名字
System.out.println(c1.getName()); // 获取包名 + 类名
System.out.println(c1.getSimpleName()); // 类名
// 获取类的属性
Field[] fields = c1.getFields(); // 只能找到public修饰的属性
Field[] declaredFields = c1.getDeclaredFields(); // 找到所有的属性
// 获取类的指定属性
System.out.println(c1.getField("name")); // 只能获取public修饰该属性对象
c1.getDeclaredField("name"); // 能够获取任意修饰符修饰该属性对象
// 获取类的方法
c1.getMethods(); // 获取本类及其父类的全部public修饰的方法
c1.getDeclaredMethods(); // 获取本类的所有方法
// 获得类的指定方法
c1.getMethod("getName",null); // 获取public修饰的指定的方法
c1.getMethod("setName",String.class); // 获取public修饰的指定的方法
c1.getDeclaredMethod("getName",null); // 获取任意修饰符修饰的指定的方法
// 获取构造器
c1.getConstructors(); // 获取public修饰的构造器
c1.getDeclaredConstructors(); // 获取所有构造器
// 获取指定的构造器
c1.getConstructor(String.class,int.class,int.class); // 获取public修饰的指定的构造器
c1.getDeclaredConstructor(String.class,int.class,int.class); // 获取任意修饰的指定的构造器
用Class对象能做什么
创建实体类对象
newInstance()方法
调用Class对象的newInstance()方法来创建对象
类必须有一个无参数的构造器
类的构造器的访问权限需要足够
// 创建User的Class对象
Class c1 = Class.forName("com.jjh.reflection.User");
// 通过Class对象构造一个User对象
User user = (User) c1.newInstance(); // 本质调用的是User类的无参构造器
System.out.println(user); // User{name='null', age=0, sex=0}
通过构造器创建对象
// 创建User的Class对象
Class c1 = Class.forName("com.jjh.reflection.User");
// 通过构造器创建对象
// 获取User类的构造器
Constructor constructor = c1.getDeclaredConstructor(String.class, int.class, int.class);
// 调用User类的构造器创建对象
User zhangsan = (User)constructor.newInstance("zhangsan", 23, 24);
System.out.println(zhangsan); // User{name='zhangsan', age=23, sex=24}
操作方法
通过反射,调用类中的方法,通过Method类完成
通过Class类的getMethod(String name,Class...parameterTypes)方法取得一个Method对象
之后使用Object invoke(Object obj, Object[] args)进行调用,并向方法中传递要设置的obj对象和参数信息。
Object 对应原方法的返回值,若原方法无返回值,此时返回null
若原方法若为静态方法,此时形参Object obj可为null
若原方法形参列表为空,则Object[] args为null
若原方法声明为private,则需要在调用此invoke()方法前,显式调用方法对象的setAccessible(true)方法,将可访问private的方法。
// 创建User的Class对象
Class c1 = Class.forName("com.jjh.reflection.User");
// 通过Class对象构造一个User对象
User user = (User) c1.newInstance(); // User{name='null', age=0, sex=0}
// 通过反射调用普通方法
// 通过反射获取一个方法对象
Method setName = c1.getDeclaredMethod("setName", String.class);
// invoke,激活setName方法(也可以理解为调用setName方法)
// 如果setName对应的方法是一个静态方法,invoke方法的参数User可以为null
// 如果setName对象对应的方法没有参数,那么invoke方法的参数"zhangsan"为null
// 如果setName对象对应的方法是private修饰的,如果想调用成功,在调用invoke方法之前,要关闭java的安全检测
// setName.setAccessible(true);
setName.invoke(user,"zhangsan");
System.out.println(user); // User{name='zhangsan', age=0, sex=0}
操作属性
// 创建User的Class对象
Class c1 = Class.forName("com.jjh.reflection.User");
// 通过Class对象构造一个User对象
User user = (User) c1.newInstance(); // User{name='null', age=0, sex=0}
// 操作属性
Field name = c1.getDeclaredField("name");
// 因为User类的name属性用private修饰,所以要关闭java的安全检查
name.setAccessible(true);
// 为user对象的name属性进行赋值"zhangsan"
name.set(user,"zhangsan");
System.out.println(user); // User{name='zhangsan', age=0, sex=0}
setAccessible
Method和Field、Constructor对象都有setAccessible()方法。
setAccessible作用是启动和禁用访问安全检查的开关
true 关闭安全检查,这样private修饰的属性、方法、构造方法也能被访问调用
false 默认为false,开启安全检查
提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true
使得原本无法访问的私有成员也可以访问
性能分析
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
test01(); // 16ms
test02(); // 1388ms
test03(); // 2623ms
}
// 普通方式
public static void test01(){
User user = new User();
long startTime = System.currentTimeMillis();
// 调用10亿次getName方法
for (int i = 0; i < 1000000000; i++) {
user.getName();
}
// 计算用时
System.out.println(System.currentTimeMillis()-startTime+"ms");
}
// 反射方式,关闭java安全检测
public static void test02() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
User user = new User();
Class c1 = user.getClass();
Method getName = c1.getDeclaredMethod("getName", null);
// 关闭安全检测
getName.setAccessible(true);
long startTime = System.currentTimeMillis();
// 调用10亿次getName方法
for (int i = 0; i < 1000000000; i++) {
getName.invoke(user,null);
}
// 计算用时
System.out.println(System.currentTimeMillis()-startTime+"ms");
}
// 反射方式,开启java安全检测
public static void test03() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
User user = new User();
Class c1 = user.getClass();
Method getName = c1.getDeclaredMethod("getName", null);
long startTime = System.currentTimeMillis();
// 调用10亿次getName方法
for (int i = 0; i < 1000000000; i++) {
getName.invoke(user,null);
}
// 计算用时
System.out.println(System.currentTimeMillis()-startTime+"ms");
}
操作泛型
Java采用泛型擦除的机制来引入泛型 , Java中的泛型仅仅是给编译器javac使用的,确保数据的安全性和免去强制类型转换问题 , 但是 , 一旦编译完成 , 所有和泛型有关的类型全部擦除。
为了通过反射操作这些类型 , Java新增了 ParameterizedType , GenericArrayType , TypeVariable和 WildcardType 几种类型来代表不能被归一到Class类中的类型但是又和原始类型齐名的类型
ParameterizedType : 表示一种参数化类型,比如Collection
GenericArrayType : 表示一种元素类型是参数化类型或者类型变量的数组类型
TypeVariable : 是各种类型变量的公共父接口
WildcardType : 代表一种通配符类型表达式
// 泛型(参数化类型)作为方法参数
public void test01(Map<String,User> map, List<User> list){
System.out.println("test01");
}
// 泛型(参数化类型)作为返回值
public Map<String,User> test02(){
System.out.println("test02");
return null;
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
Class c1 = Class.forName("com.jjh.reflection.Test07");
// 参数化类型作为方法参数
Method test01 = c1.getDeclaredMethod("test01", Map.class, List.class);
// 获取方法参数的泛型参数类型
Type[] genericParameterTypes = test01.getGenericParameterTypes();
for (Type genericParameterType : genericParameterTypes) {
// 如果泛型参数类型(genericParameterType)属于参数化类型(ParameterizedType)
if(genericParameterType instanceof ParameterizedType){
// 强制转换为参数化类型,然后调用getActualTypeArguments方法获取真实的类型
Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println(actualTypeArgument);
/**
* 输出结果:
* class java.lang.String
* class com.jjh.reflection.User
* class com.jjh.reflection.User
* 对比Test01方法: public void test01(Map<String,User> map, List<User> list)
*/
}
}
}
// 参数化类型作为方法的返回值
Method test02 = c1.getDeclaredMethod("test02", null);
// 获取方法返回值的泛型返回值类型
Type genericReturnType = test02.getGenericReturnType();
// 如果泛型返回值类型属于参数化类型,则进行强转获取真实类型
if(genericReturnType instanceof ParameterizedType){
// 获取泛型返回值类型的真实类型
Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println(actualTypeArgument);
}
/**
* 输出结果:
*class java.lang.String
* class com.jjh.reflection.User
* 与test02方法对比:public Map<String,User> test02()
*/
}
}
操作注解
getAnnotations、getAnnotation
举例:
@Tablejjh注解:
/**
* 定义注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Tablejjh{
String value();
}
@Filedjjh注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Filedjjh{
String columnName();
String type();
int length();
}
Student1类:
/**
* Student1类:
* 属性:
* int id;
* int age;
* ing name;
* 方法:
* 有参、无参、setter、gettter、toString
*/
@Tablejjh("db_student")
class Student1{
@Filedjjh(columnName = "db_id",type = "int",length = 10)
private int id;
@Filedjjh(columnName = "db_age",type = "int",length = 3)
private int age;
@Filedjjh(columnName = "db_name",type = "varchar",length = 10)
private String name;
.......
}
获取作用在类上的注解
Class c1 = Class.forName("com.jjh.reflection.Student1");
// 获取作用在Student1类上的所有注解
Annotation[] annotations = c1.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation); // @com.jjh.reflection.Tablejjh(value=db_student)
}
// 获取Tablejjh注解的value值
// 方式1:使用instanceof判断是否为指定注解
for (Annotation annotation : annotations) {
// 判断是否为指定注解
if(annotation instanceof Tablejjh){
// 如果是指定的注解,进行强制类型转换,然后.value()获取作用在该类上的此注解的值
System.out.println(((Tablejjh) annotation).value()); // db_student
}
}
// 方式2:使用getAnnotation(Class class)获取指定注解
Tablejjh annotation = (Tablejjh) c1.getAnnotation(Tablejjh.class);
System.out.println(annotation.value()); // db_student
获取作用在字段上的注解
Class c1 = Class.forName("com.jjh.reflection.Student1");
// 获取Student1上的name字段对象
Field name = c1.getDeclaredField("name");
// 获取作用在字段上的注解
Annotation[] annotations = name.getAnnotations();
for (Annotation annotation : annotations) {
// @com.jjh.reflection.Filedjjh(columnName=db_name, type=varchar, length=10)
System.out.println(annotation);
}
// 获取作用在字段上的注解值
// 方式1:instanceof判断是否是指定注解
for (Annotation annotation : annotations) {
if(annotation instanceof Filedjjh){
// 如果是指定的注解,先强制类型转换,在获取注解的值
System.out.println(((Filedjjh) annotation).columnName()); // db_name
System.out.println(((Filedjjh) annotation).type()); // varchar
System.out.println(((Filedjjh) annotation).length()); // 10
}
}
// 方式2:使用getAnnotation(Class class)方法
Filedjjh annotation = name.getAnnotation(Filedjjh.class);
System.out.println(((Filedjjh) annotation).columnName()); // db_name
System.out.println(((Filedjjh) annotation).type()); // varchar
System.out.println(((Filedjjh) annotation).length()); // 10
14章 正则表达式
校验某字符串是否符合指定的规则
匹配规则
1.一个字符且仅限一个字符:".", a.c→a&c、acc、abc
2.匹配数字:"\d",00\d→007、008、009 匹配非数字:"\D",00\D→00A
3.匹配一个字母、数字或下划线:"\w",java\w→javac、java9、java_ \W可以匹配\w不能匹配的字符
4.空格,tab字符:"\s",a\sc→a c、a c \S可以匹配\s不能匹配的字符
重复匹配
1.匹配任意个字符,包括0个字符:"",A\d →A、A0、A380
2.匹配至少一个字符:"+",A\d+→A0、A380
3.匹配0个或一个字符:"?",A\d?→A、A0
4.精确指定n个字符:"{n}",A\d{3}→A380
5.指定匹配n~m个字符:"{n,m}",A\d{3,5}→A380、A3800、A38000
6.没有上限:"{n,}",匹配至少n个字符。
15章 枚举(尚硅谷)
枚举类的使用
枚举类的理解:类的对象只有有限个,确定的。我们称此类为枚举类
当需要定义一组常量时,强烈建议使用枚举类
如果枚举类中只有一个对象,则可以作为单例模式的实现方式
定义枚举类
自定义枚举类
jdk5.0之前,自定义枚举类
public class SeasonTest {
public static void main(String[] args) {
System.out.println(Season.SPRING);
}
}
class Season{
// 声明season对象的属性
private final String seasonName;
private final String seasonSesc;
// 构造器私有化
public Season(String seasonName, String seasonSesc) {
this.seasonName = seasonName;
this.seasonSesc = seasonSesc;
}
// 提供当前枚举类的多个对象
public static final Season SPRING=new Season("春天","春暖花开");
public static final Season SUMMER=new Season("夏天","烈日炎炎");
public static final Season AUTUMN=new Season("春天","秋高气爽");
public static final Season WINTER=new Season("春天","寒风阵阵");
// 其他诉求,获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}
public String getSeasonSesc() {
return seasonSesc;
}
// toString方法
@Override
public String toString() {
return "Season{" +
"seasonName='" + seasonName + '\'' +
", seasonSesc='" + seasonSesc + '\'' +
'}';
}
}
enum关键字定义枚举类
jdk5.0,可以使用enum关键字定义枚举类
public class SeasonTest {
public static void main(String[] args) {
// 调用的是父类Enum的toString方法,获取枚举类对象常量的名称
System.out.println(Season.SPRING);
System.out.println(Season.getSeasonByName("春天"));
System.out.println(Season.valueOf("SPRING"));
}
}
enum Season {
// 声明当前枚举类有限个对象
SPRING("春天","春暖花开"),
SUMMER("夏天","烈日炎炎"),
AUTUMN("春天","秋高气爽"),
WINTER("春天","寒风阵阵");
// 声明属性
private final String seasonName;
private final String seasonSesc;
// 构造方法
Season1(String seasonName, String seasonSesc) {
this.seasonName = seasonName;
this.seasonSesc = seasonSesc;
}
public String getSeasonName() {
return seasonName;
}
public String getSeasonSesc() {
return seasonSesc;
}
public static Season getSeasonByName(String name){
// values() 获取枚举类对象,返回数组
Season1[] values = Season1.values();
for (Season season : values) {
if(season.getSeasonName().equals(name)){
return season;
}
}
return null;
}
}
枚举类中的常用方法
values()
返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值
valueOf(String str)
可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对像的“名字”。如果不是,会有运行时异常。
toString( )
返回当前枚举类对象常量的名称
枚举类实现接口
枚举类本身实现
枚举类的对象统一执行类中重写接口的方法
public class SeasonTest {
public static void main(String[] args) {
// System.out.println(Season.SPRING);
// System.out.println(Season.getSeasonByName("春天"));
// System.out.println(Season.valueOf("SPRING"));
Season spring = Season.valueOf("SPRING");
spring.show();
}
}
// 定义接口
interface Info{
void show();
}
// 定义枚举,实现接口
enum Season implements Info{
// 声明当前枚举类有限个对象
SPRING("春天","春暖花开"),
SUMMER("夏天","烈日炎炎"),
AUTUMN("春天","秋高气爽"),
WINTER("春天","寒风阵阵");
// 声明属性
private final String seasonName;
private final String seasonSesc;
// 构造方法
Season1(String seasonName, String seasonSesc) {
this.seasonName = seasonName;
this.seasonSesc = seasonSesc;
}
public String getSeasonName() {
return seasonName;
}
public String getSeasonSesc() {
return seasonSesc;
}
public static Season getSeasonByName(String name){
// values() 获取枚举类对象,返回数组
Season1[] values = Season1.values();
for (Season season : values) {
if(season.getSeasonName().equals(name)){
return season;
}
}
return null;
}
// 重写接口方法
@Override
public void show() {
System.out.println("这是一个季节");
}
}
输出结果:
这是一个季节
枚举类对象实现
每个对象各自执行自己实现接口的方法
package com.jjh;
public class SeasonTest {
public static void main(String[] args) {
// System.out.println(Season.SPRING);
// System.out.println(Season.getSeasonByName("春天"));
// System.out.println(Season.valueOf("SPRING"));
Season spring = Season.valueOf("SPRING");
spring.show();
}
}
interface Info{
void show();
}
enum Season implements Info {
// 声明当前枚举类有限个对象
SPRING("春天","春暖花开"){
@Override
public void show() {
System.out.println("这是一个春天");
}
},
SUMMER("夏天","烈日炎炎"){
@Override
public void show() {
System.out.println("这是一个夏天");
}
},
AUTUMN("秋天","秋高气爽"){
@Override
public void show() {
System.out.println("这是一个秋天");
}
},
WINTER("冬天","寒风阵阵"){
@Override
public void show() {
System.out.println("这是一个冬天");
}
};
// 声明属性
private final String seasonName;
private final String seasonSesc;
// 构造方法
Season(String seasonName, String seasonSesc) {
this.seasonName = seasonName;
this.seasonSesc = seasonSesc;
}
public String getSeasonName() {
return seasonName;
}
public String getSeasonSesc() {
return seasonSesc;
}
// 遍历枚举类对象
public static Season getSeasonByName(String name){
Season[] values = Season.values();
for (Season season : values) {
if(season.getSeasonName().equals(name)){
return season;
}
}
return null;
}
}
输出结果:
这是一个春天
总结
16章 XML与JSON
XML
可扩展标记语言(eXtensible Markup Language)的缩写,它是一种数据表示格式,可以描述非常复杂的数据结构,常用于传输和存储数据。
<?xml version="1.0" encoding="UTF-8" ?>
{
"id": 1,
"name": "Java核心技术",
"author": {
"firstName": "Abc",
"lastName": "Xyz"
},
"isbn": "1234567",
"tags": ["Java", "Network"]
}
描述
默认使用UTF-8编码
首行必定是,声明的是文档定义类型(可选),可以指定一系列规则
一个XML文档有且仅有一个根元素,根元素可以包含任意个子元素
特殊符号,需要使用&???;表示转义,Java→Java<tm>
真正的“根”是document,代表XML文档。
解析
DOM解析:一次性读取XML,并在内存中表示为树形结构,主要缺点是内存占用太大。优点是用起来省事
Document:代表整个XML文档,Element:代表一个XML元素,Attribute:代表一个元素的某个属性
举例
InputStream input = Main.class.getResourceAsStream("/book.xml");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
//ocumentBuilder.parse()用于解析一个XML,它可以接收InputStream,File或者URL
Document doc = db.parse(input);
SAX解析:边读取XML边解析,并以事件回调的方式让调用者获取数据。所以无论XML有多大,占用的内存都很小。
startDocument:开始读取XML文档;startElement:读取到了一个元素,例如;characters:读取到了字符;endElement:读取到了一个结束的元素,例如 ;endDocument:读取XML文档结束。
举例
InputStream input = Main.class.getResourceAsStream("/book.xml");
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser saxParser = spf.newSAXParser();
saxParser.parse(input, new MyHandler());
// 除了需要传入一个InputStream外,还需要传入一个回调对象,这个对象要继承自DefaultHandler
SAXParser.parse()
class MyHandler extends DefaultHandler {}
4.Jackson解析(小点代码.txt): 接从XML文档解析成一个JavaBean
JSON
{
"id": 1,
"name": "Java核心技术",
"author": {
"firstName": "Abc",
"lastName": "Xyz"
},
"isbn": "1234567",
"tags": ["Java", "Network"]
}
优点
JSON只允许使用UTF-8编码,不存在编码问题;
JSON只允许使用双引号作为key,特殊字符用\转义,格式简单;
浏览器内置JSON支持,如果把数据用JSON发送给浏览器,可以用JavaScript直接处理。
支持以下几种数据类型
键值对:
数组:[1, 2, 3]
字符串:"abc"
数值(整数和浮点数):12.34
布尔值:true或false
空值:null
解析
解析JSON:Jackson、Gson、Fastjson(小点代码.txt)