Java上 常用的dos命令
作用
命令
切换盘符
盘符名:->
查看目录下文件
dir
跳转文件夹
cd
清空
cls
退出
exit
创建文件夹
mkdir
删除文件夹
rd
删除文件
del
jvm和跨平台
1 2 1、jvm:Java运行程序的假想虚拟机,主要用来运行Java程序 2、跨平台:Java代码可以在不同的操作系统上运行
java是运行命令,javac是编译命令,跳过让人头疼的环境变量吧
写一个helloword 1 2 3 4 5 6 7 1、编写 a.新建一个.java文件 2、编译 a.命令javac java文件名.java b.javac会将Java文件编译生成一个.class文件,jvm只认识.class文件 3、运行 a.命令:Java class文件名
这里我一直错误,配置了好久的环境变量,结果不是环境变量的问题
输入运一下java -cd ./ Hello 即可,神经
注释
有三种注释方法
解释一下之前的入门程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Hello { public static void main (String[] args) { System.out.println("Hello World" ); } }
养成语句结束使用分号的好习惯
字符编码问题 1 2 3 4 5 6 7 1、保存数据的过程就是编码的过程 2、读取数据的过程就是解码的过程 3、编解码要是同一个编码 常见的编码规范: GDK:专门为中文设计的编码(ANSI),一个中文汉字占两个字节 utf-8:一个中文汉字占三个字节 dos窗口默认编码为GDK
源文件名与类目一致问题 并不是必须的,如果不一致,需要将class前的public删掉
javac编译生成的class文件名称与类名相同
一个Java文件中可以有多个类名,但是只能有一个类带public,建议不要在一个文件中随意写class
main方法必须写在带public的类中
println和print的区别 1 2 3 相同点:都是输出语句 不同点:println输出后带换行效果;相当于python中print print输出后不带换行效果;相当于python中的print(end='')
常量、变量、类型转化 常量 在代码的运行过程中其值不会发生改变的数据
分类:
整数常量:所有整数
小数常量:带小数点就算
字符常量:带单引号的,单引号中必须有且只能有一个内容 '1'(算) '11'(不算) ''(不算) ' '(算) ' '(不算)
单引号内打一个Tab键也算
字符串常量:带双引号的,双引号内容随意
布尔常量:true和false
空常量:null(不能直接使用)
注意一下常量的运算 除法是:如果前后都是整数那么,结果只取整数部分
前后只要有一个带小数点,结果就是正常小数
数据类型 1 2 3 4 5 6 7 1、基本数据类型:四类八种 整型:byte、short、int、long 浮点型:float、double 字符型:char 布尔型:boolean 2、引用数据类型 类、数组、接口、枚举、注解
数据类型
关键字
内存占用
数值范围
字节型
byte
1字节
-128至127
短整型
short
2字节
-32768至32767
整型
int
4字节
-2^31^ 至 2^31^ 正负21亿
长整型
long
8字节
-2^63^ 至 2^63^
单精度浮点数
float
4字节
1.4013E-45 至 1.4013E+38 1.4乘十的负45次方
双精度浮点数
double
8字节
4.9E-324 至 1.7977E+308
字符型
char
2字节
0 至 2^16^-1
布尔类型
boolean
1字节
true 和 false
注意:
定义Long类型是后面要加一个大写的L
定义float类型时后面加上一个F
float 和 double的区别
float的小数位只有23位二进制,能表示最大的十进制位(8388608),七位数
double的小数位有52位,能表示最大的十进制位(4 503 599 627 370 496),十六位数
==不要用float和double直接参与运算==,精度不准
输出结果为 1.4300001
变量 在代码运行过程中,值会随着不同的情况而随时发生改变的数据
定义方法
1 2 3 4 5 6 7 8 9 10 11 1、数据类型 变量名 = 值 2、数据类型 变量名; 变量名 = 值 3、数据类型 变量1 变量2 变量3; 变量1 = 值 变量2 = 值 变量3 = 值 4、数据类型 变量名1 = 值, 变量名2 = 值, 变量名3 = 值;
字符串属于引用数据类型,用String表示,String是一个类,只不过是字符串定义是和基本数据类型定义格式一样
变量没赋值,没有初始化无法使用,在同一个作用域中不能定义重名的变量
不同作用域的变量不要随意访问
小作用域中可以访问大作用域的变量
大作用域中不可以访问小作用域的变量
转义字符 \
:将普通字符转换成具有特殊含义的字符,将带有特殊含义的字符转换为普通字符
标识符 给类、方法、变量取的名字
1 2 3 4 5 6 7 8 9 注意: 硬性规定: 标识符可以包含数字,字母 $_ 标识符不能以数字开头 标识符不能是关键字 软性建议: 给类取名字是遵循大驼峰式命名 -> 每个单词首字母大写 给方法和变量遵循小驼峰式命名 -> 从第二个单词开始大写 见名知意
数据类型转换 1 2 3 4 5 6 7 什么时候发生数据类型转换: 等号两边类型不一致的时候,如:long和float定义值时需要加L、F,如果不加的话会类型转化 不同类型的数据做运算 基本数据类型按照取值范围从小到大排序 byte,short,char -> int -> long -> float -> double
自动类型转换 将取值范围小的数据类型赋值给取值范围大的数据类型会自动数据转换 -> 小转大
取值范围小的数据类型和取值范围大的数据类型做运算 -> 小转大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Hello { public static void main (String[] args) { long num1 = 100 ; System.out.print(num1); int i = 10 ; double x = 2.5 ; double sum = i + x; System.out.print(sum); } }
强制类型转换 当取值范围大的数据类型赋值给取值范围小的数据类型 -> 强制转换
1 2 3 4 取值范围小的数据类型 变量名 = (取值范围小的数据类型)取值范围大的数据类型 // 右边的数据是小数,小数默认类型为double float num = (float)2.5; float num = 2.5F;
注意,不要随意写成强制转换的形式,会有精度损失问题以及数量溢出现象
精度损失问题
1 2 int i = (int)2.9 // 这个时候 i 的值为2,0.9损失掉了,不是四舍五入,而是抹零
数据溢出
1 2 3 4 5 6 7 8 9 10 11 int j = (int )10000000000L ; System.out.print(j);
但是也有不得不用强制转换的时候例如
1 2 3 调用find()功能获取2.5这个数据,以为find()要求必须使用float型的变量接收 float number = 对象.find()
数字默认为int
byte 和 short 定义的时候如果等号右边是常量,但是取值在范围内,不需要强转,jvm会自动转型
byte 和 short 如果==等号右边有变量参与==,byte和short自动提升为int,然后结果再次赋值给byte和short,需要我们自己手动强转
1 2 3 4 5 6 7 8 9 10 byte i = 10; System.out.print(i) // 输出没问题 i = i + 1; System.out.print(i) // 这个时候就会给出提示,从int转化为byte可能会有损失 i = (byte)(i+1) // 转化
char类型数据,如果参与运算会自动提升为int类型
如果char转换为int类型,如果是字母会去ASCII码表中去查,对应数字
如果在ASCII码表中没有找到对应字符,那么就会去Unicode(万国码)码表中找
byte类型强转有一点事
1 2 byte i = (byte)200 System.out.print(i) // 输出-56
这里涉及到二进制的源码、反码、补码的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 200,转换成二进制 原码:1100 1000 反码是原码第一位不变,其余0、1互换 反码:1011 0111 补码是在反码的基础上加一 补码:1011 1000 注意一下,因为byte的取值范围为 -128至127,所以正数的时候第一位是一直为0,因此,第一位代码的0、1设计为了正负的判断,第一位为1是负数,为0是正数 现在再看补码,除去第一位是56,第一位是1,因此结果为-56 还有一种更简单的理解方式 byte i = (byte)128 这个时候也是超出范围的输出结果为 -128 再结合200找一下规律,发现也可以理解为 byte 的数是从 -128至127循环的,128就变为了 -128
位运算符
位运算符
符号解释
&
按位与,当两位相同时为1才返回1,and
`
`
~
按位非,将操作数的每个位(包括符号位)全部取反
^
按位异或,当两位相同时返回0,不同时返回1
<<
左移运算符
>>
右移运算符
>>>
无符号右移运算符
1 2 3 4 5 6 7 & -> 有假则假 | -> 有真则真 ~ -> 取反 ^ -> 前后结果一样为false,不一样为true << -> 左移几位相当于乘以2的几次方,很好理解吧,左移指的是二进制的左移 >> -> 右移几位相当于乘以2的负几次方,向下取整,理解为二进制右移即可 >>>
计算机在存储数据的时候都是存储的数据的补码,计算也用的补码,但是最终看到的结果是原码,正数最高位为0,负数最高位为1
如果是正数,原反补是一致的,负数的原反补之前在byte是写过,我的理解是128八位二进制时最高位为1,对于byte来说只有八位二进制,因此使用了负数的原反补形式
右移 说一下右移,除了==负数的右移是补1,其他的都是补0==
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 9>>2 结果为2 0000 1001右移两位,补零,成为 0000 0010 正数还好,看一下负数 -9>>2 -9的原码 1000 …0… 0000 1001 -9的反码 1111 …1… 1111 0110 -9的补码 1111 …1… 1111 0111 补码右移两位,这个时候左边空了,但是不能补0,因为补0就是正数了,因此要补1 1111 …1… 1111 1101 根据补码算反码 1111 …1… 1111 1100 反码推原码 1000 …0… 0000 0011 结果为-3
无符号右移 向右移动后,==左边空出来的位直接补0==,无论正负,无符号还是很生动形象的
负数
1 2 3 4 5 6 7 -9>>>2 补码成为了这个,中间很多1,变为原码时数字会变得很大,因为第二位会是1 0011 …1… 1111 1101 结果为 1073741821
这里有个很有意思的事情
1 2 3 8>>>32 -> 8一共只有32为,右移32为结果是不变的,意思是右移32为相当于没移 8>>>34 -> 8右移了两位
位运算符 与& 运算规则:对应位都是1才是1,相当于符号左右两边都是true,结果才是true
有个比较抽象的地方
1&1 结果为1
0&0 结果为0
1&0 结果为0
这都很好理解
1 2 3 4 5 6 7 8 9 10 11 12 13 5&3 结果为1 用二进制解释一下: 5、3的二进制 0000 0101 0000 0011 同0为0,不同为0,同1为1 0000 0001 因此 5&3为 0000 0001 结果为1
或| 运算规则:
有1则1,同0则0
1|1 >>>> 1
0|1 >>>> 1
0|0 >>>> 0
1 2 3 4 5 6 7 8 5|3 5、3的二进制 0000 0101 0000 0011 0000 0111 结果为7
异或^ 运算规则:相同为0,不同为1
1^1 0
0^1 1
1 2 3 4 5 6 7 5^3 0000 0101 0000 0011 0000 0110 结果为6
按位取反 运算规则:0就是1,1就是0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ~10 结果为-11 10为正数 0000 …0… 0000 1010 ~10 按位取反后的补码 1111 …1… 1111 0101 反码 1111 …1… 1111 0100 原码 1000 …0… 0000 1011 结果为-11
运算符的优先级 1、表达式不要太复杂,有小括号先算小括号里的内容 2、
使用开发工具idea idea 的目录结构
一个根目录project(项目)
项目下面会有子文件夹module(模块)
模块下面的文件夹叫做package(包)package需要放到模块文件夹下的src目录下
package取名规范
1 2 3 域名倒着写 比如:www.sdpc.com 写为:com.sdpc 来创建包
而创建包的时候就会发现com.sdpc不是一个文件夹,而是嵌套文件夹com/sdpc
快捷键 AIt + Enter 快速修复
Ctrl + d 复制粘贴行
Ctrl + k 格式化代码
ctrl + / 单行注释
Ctrl + shift + / 多行注释
10.var 可以直接生成等号左边的内容
运算符 简单的运算符直接省略了
自增自减运算符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 1、格式 模式: 变量++ -> 后自加 ++变量 -> 前自加 变量-- -> 后自减 --变量 -> 前自减 自增自减只变化1 2、使用 a.单独使用 ++ -- 单独为一句,不和其他语句混合使用 i++; 符号在前在后都是先运算 b.混合使用 ++ -- 和其他语句掺和使用(如:输出语句,赋值语句) 符号在前:先运算、再使用运算后的值 int j = 100; int i = ++j; 这个时候分别输出i和j输出结构均为101,先运算 符号在后:先使用原值,使用原值后再运算 int x = 10; int y = x++; 这个时候分别输出y和x输出结果分别为10,11,先输出,再运算
逻辑运算符 1 2 作用:连接多个Boolean结果 结果:Boolean类型
符号
说明
&&(与,和)
有假则假,and
||(或者)
有真则真,or
!(非,取反)
取反,true就是false
^(异或)
结果一样是false,不一样为true
区别一下单与和双与
符号
说明
&
1、单与,如果前后都是布尔型,有假则假,第一个运算为false时。第二个仍会继续运算 2、如果判断的是数字,看作是位运算符,算二进制
&&
双与,有假则假,但是有短路效果,第一个运算为false时,会停止运算
|
1、单或,有真则真,第一个运算为true时。第二个仍会继续运算 2、如果前后都是数字,看作位运算符
||
双或,有真则真,第一个运算为true时,会停止运算
可以用代码验证一下
1 2 3 4 5 6 7 8 9 int a = 10 int b = 20 boolean c = (++a>100) & (++b>10) System.out.print(a,b) // 输出结果位11,21 boolean c = (++a>100) && (++b>10) System.out.print(a,b) // 输出结果为11,20
Java内使用逻辑运算符拼接各种运算
三元运算符 1 2 3 4 5 6 7 8 9 格式: boolean 表达式?表达式1:表达式2 执行流程: 先判断,如果是true,则执行表达式1,如果是false则执行表达式2 使用三元运算符时 经常会提示我可以使用,Math.max(表达式1,表达式2)的形式来替换三元运算
流程控制语句 Scanner键盘录入 功能相当于python的input,比input麻烦
1 2 3 4 5 6 7 8 9 10 11 12 1、Scanner:Java定义好的一个类 2、作用:将数据通过键盘录入的方式放到代码中参与运行 3、位置:java.util 4、使用: a.导包(引入/import) import java.util.Scanner; 位置在类的上面 b.创建对象 Scanner 变量名 = new Scanner(System.in); c.调用方法 变量名.nextInt() 输入整数int的 变量名.next() 输入字符串String的
next 和 nextLine 的区别
next 遇到空格和回车就结束了,不能接收空格
nextLine 遇到回车结束
一个有意思的事情
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.sjjws.a;import java.util.Scanner;public class HelloWorld { public static void main (String[] args) { Scanner input = new Scanner (System.in); String i = input.next(); String i2 = input.nextLine(); System.out.println(i); System.out.println(i2); } }
这里我只录入了一次,缺分别给i和i2赋值了,因此,尽量不要将next和next Line连起来使用
Random随机数 1 2 3 4 random老朋友了 也是Java自带的一个类 位置在:Java.util 使用方法和Scanner类似
1 2 3 4 5 6 7 8 public class HelloWorld { public static void main (String[] args) { Random sum = new Random (); int date = sum.nextInt(); System.out.println(date); } }
设置随机范围
1 2 int date = sum.nextInt(); // 和python类似,写一个x 从0开始到x-1结束
流程控制语句 switch 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 格式: switch(变量){ case 常量值1: 执行语句1; break; case 常量值2: 执行语句2; break; ………… default: 执行语句n; break; } 执行流程: 用变量接收的值和下面case后面的常量值匹配,匹配上哪个case就执行哪个case对应的执行语句 如果都没有匹配上,就执行default对应的语句 break:结束关键字 switch可以匹配到的数据类型:byte、short、int、char、枚举、String
case有穿透性
1 如果没有break,就会出现case的穿透性,程序就一直往下穿透运行,直到遇到break或者switch代码执行完毕,停止
if 1 2 3 4 5 6 7 8 9 10 11 12 if的一些操作懒得说了,说点不一样的,第一个如果使用if比较的数据类型为布尔类型,可以只写一个等号 if两种情况以上的判断 if(){ } else if(){ } else { } // 最后不一定使用else但是要保证所有情况都有
switch和if的区别 打个断点看一下,右键==调试==,看一下if判断的流程,发现有高亮有暗点,可以发现if从上往下便利到if等于三时,if判断结束,再看一下switch,switch会直接跳到匹配的case
for循环 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 格式: for(初始化变量;比较;步进表达式){ 循环语句 } 执行流程: a.先走初始化变量 b.比较如果为true,走循环语句,走步进表达式(初始化变量) c.再比较,如果还是true,继续走循环语句,走步进表达式 d.比较,如果为false,结束 先循环,再加一 循环快捷键 次数.fori
do……while循环 1 2 3 4 5 6 7 8 9 10 11 12 13 格式: do{ 循环语句; 步进表达式; } while (比较); 1、初始化变量 2、循环语句 3、步进表达式 4、判断循环 do……while的特点是至少循环一次,先循环再判断
数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 特点: 即可以储存基本类型数据,还能引用储存的数据 定长(定义数组时长度为多长,最多就能存多少数据) 定长是数组最大的特点,也是最大的缺点 定义: 1、动态初始化:在定义数组的时候,我们没有给具体的数据,只指定了长度 数据类型[] 数组名 = new 数据类型[长度] 等号左边的数据类型:规定了数组中只能存储什么类型的数据 []:代表的是数组,一个[]代表一维数组,两个[][]代表二维数组 new:代表创建数组 等号右边的数据类型要和左边的数据类型一致 [长度]:表示数组最多能存贮多少数据 2、静态初始化:在定义数组的时候,我们直接给出数据 数据类型[] 数组名 = new 数据类型[]{元素1,元素2,……} 数据类型 数组名[] = new 数据类型[]{元素1,元素2,……} 静态初始化的简化: 数据类型[] 数组名 = {元素1,元素2,……}
数组操作 在数组中数据的存储、查询都需要指定索引
获取数组长度 1 2 3 4 数组名.length 注意: length后面不要带小括号,因为length不是数组中的方法,而是数组中的一个属性
获取元素 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 元素赋值格式 数组名[索引] = 值 获取元素 数组名[索引] 注意点: 直接输出数组名,会输出数组在内存中的地址值 地址值:数组在内存中的一个身份证号,唯一标识,我们可以通过这个唯一标识在内存中找到这个数组,从而操作这个数组中的数据 如果数组中没有数据,那么也是可以获取到默认的数据 整数:0 小数:0.0 字符:'\u0000' 对应是int值是0 布尔:false 引用:null 遍历数组的快捷方式 数组名.fori
数组操作的常见异常 数组索引越界异常_ArrayIndexOutOfBoundsException 1 2 3 操作的索引超过数组的长度范围 注意Java的索引是没有负数的,不想python那样-1表示倒数第一个元素
空指针异常_NullPinterException
数组扩容 1 2 3 4 5 6 7 8 9 10 新建一个数组,将老数组的元素赋值给新数组,然后将老数组的地址值改为新数组的地址值 int[] old = {1,2,3,4,5} int[] now = new int[10]; for…… old = now 但是我就产生了一个问题,新建的数组地址值是多少,去哪了, 结果是,新老两个数组此时的地址值是相同的
内存图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 1、内存可以理解为“内存条”,所有的软件,程序运行起来都会进入到内存中,占用内存,在Java的世界中,将内存划分为了五块 2、 a.栈(重点)(Stack) 主要运行方法,方法的运行都会进入栈内存运行,运行完毕后,需要“弹栈”,为了腾空间 b.堆(重点)(Heap) 保存的是对象,数组,每new一次,都会在堆内存中开辟空间,并为这个空间分配一个地址值,堆中的数据都是有默认值的 整数:0; 小数:0.0; 字符:“\u0000”; 布尔:false; 引用:null; c.方法区(重点)(Method Area) 代码的“预备区”,记录了类的信息以及方法的信息 方法区中主要保存class文件以及其中的信息 代码运行之前,需要先进内存(方法区) d.本地方法栈(了解)(Native Method Stack):专门运行native方法(本地方法) 本地方法可以理解为堆对Java功能的补充 有很多功能Java实现不了。所有需要依靠本地方法完成(C语言编写) e.寄存器(了解)(pc register) -> (和CPU有关)
现在再回顾一下之前的数组扩容操作,新老数组的地址值是一样的,这个时候两个数组同时指向了堆内存的同一个空间,这个时候,更改任意一个数组另一个数组都会变化
二维数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 概述:数组中嵌套数组 格式: 1、动态初始化 数据类型[][] 数组名 = new 数据类型[m][n] 数据类型 数组名[][] = new 数据类型[m][n] 数据类型[] 数组名[] = new 数据类型[m][n] m:代表的是二维数组的长度 n:代表的是二维数组内每个一维数组的长度 2、静态初始化 数据类型[][] 数组名 = new 数据类型[][]{{元素1,元素2……},{元素1,元素2……}……} 数据类型 数组名[][] = new 数据类型[][]{{元素1,元素2……},{元素1,元素2……}……} 数据类型[] 数组名[] = new 数据类型[][]{{元素1,元素2……},{元素1,元素2……}……} 简化静态初始化: 数据类型[][] 数组名 = {{元素1,元素2……},{元素1,元素2……}……} 数据类型 数组名[][] = {{元素1,元素2……},{元素1,元素2……}……} 数据类型[] 数组名[] = {{元素1,元素2……},{元素1,元素2……}……}
方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 之前的所有代码都在main方法中写,如果我们将来所有的功能代码都放到main方法内,会显得main方法代码太多,太乱,不便于维护 将对应的代码封装成方法,使用时调用即可 格式: 修饰符 返回值类型 方法名(参数){ 方法体 return 结果 } 修饰符:public static(这个并不是固定搭配,public表示该成员变量或方法是公开的,可以被任何其他类访问,static表示成员变量或方法是静态的) 返回值类型: 该方法返回的结果的数据类型,无返回值为 void 比如:return 1 -> int 可以分为四种方法: 无参无返回值方法 有参无返回值方法 无参有返回值方法 有参有返回值方法
定义方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 无参无返回值方法 public static void 方法名(){ 方法体 } void 方法表示无返回值,写了void就不要写(return 结果)了,只写return是没问题的; 有参无返回值方法 public static void 方法名(数据类型 变量名){ 方法体 } 无参有返回值方法 public static 返回值类型 方法名(){ 方法体 return 返回值 } 有参有返回值方法 public static 返回值类型 方法名(参数){ 方法体 return 返回值 }
形参和实参 1 2 形式参数(形参):在定义方法的时候形式上定义的参数,此时参数还没有值 实际参数(实参):在调用方法的时候给形参赋予的具体的值
三层架构思想
数组作为参数 传递到方法中
1 定义的int[],就传int[],就接收int[]
数据本身是引用数据类型,引用数据类型传入的是地址值
返回值也是数组时,返回值也是地址值
重载方法 如果,我写一个加法的方法,但是会有不同的需要,比如两个数相加,三个数相加,四个数相加,这个时候再去找哪些定义的那些方法会很麻烦,我们可以通过方法的重载,来达到节省方法名的目的
1 2 3 4 如:我写一个方法名称是sum,传递两个参数,再写一个方法名称仍然是sum,传递三个参数,以此类推,当我向调用时。只需要传入不同的参数个数,就可以找到相应的方法 方法名相同,参数列表不同的方法——重载方法
重载方法主要通过参数的不同来区分,什么叫做参数列表不同
只要是参数列表不同的,都可以重载,可以共存
判断不可以重载的与什么无关
面向对象 1 2 3 4 5 6 7 8 9 1、面向过程:自己的事情自己干,代表语言C语言 2、面向对象:自己的事情要别人帮忙干,代表Java 很多功能都是被封装好的,我们只需要拿过来直接使用即可,简化了编写过程,减少代码量 之前使用的获取键盘录入和随机数字就是面向对象的一种体现 这里回忆一下当时的使用方法,可以看到这里是引入,然后赋值,然后创建对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.sjjws.a;import java.util.Random;import java.util.Scanner;public class GetNumGame { public static void main (String[] args) { Scanner input = new Scanner (System.in); Random sum = new Random (); int num = sum.nextInt(1 ,101 ); for (int i = 0 ;; i++) { try { System.out.print("请输入数字:" ); int number = input.nextInt(); if (number == num) { System.out.println("第" +(i+1 )+"次,恭喜猜对了,答案为" +num); break ; } else if (number > num){ System.out.println("第" +(i+1 )+"次,数字偏大了" ); continue ; } else { System.out.println("第" +(i+1 )+"次,数字偏小了" ); continue ; } } catch (Exception e){ System.out.println("请输入正确的数字" ); input.next(); i--; continue ; } } } }
通过图片可以发现,这里没有static,只要是定义的时候没有static的,调用时都需要new,如果调用时带static,就不需要new,直接类目即可
类和对象 类
1 2 3 类class 测试类:带main方法,主要是运行代码的 实体类:是一类事务的抽象表示形式
1 2 3 4 5 6 7 8 组成部分: 1、属性(成员变量)(全局变量) a.定义位置:类中方法外 b.作用范围:当前类 c.定义格式:数据类型 变量名 d.有默认值 2、行为(成员方法) 只需要将static干掉,就是成员方法
对象
1 2 3 4 5 6 7 8 9 概述:一类事物的具体体现 使用: 1、导包(如果在同一个包下,不需要导包。 特殊包:使用java.long下的,不需要导包) 2、创建对象 用哪个类的成员,就new哪个对象 类名 对象名 = new 类名() 3、调用成员 想要使用哪个类中的成员,就用哪个类的对象.成员
匿名对象 1 2 3 4 5 6 7 8 9 10 正常的对象: int i = 10 Random p = new Random() 所谓的匿名对象:其实就是没有等号左边的部分,只有等号右边的部分(对象) 使用: new 成员() 注意,如果只是调用一下方法,可以直接匿名调用,但是如果有赋值操作的话,不能使用这个操作
一个对象调用时的内存图
new一下就会在堆中开辟,新的地址,互不干涉
全局变量(成员变量)和局部变量的区别 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1、定义位置不同 2、初始化值不同 成员变量有默认值可以不初始化直接使用 局部变量没有默认值,必须赋值后使用 3、作用范围不同 一二三都很熟悉了,懒得说,还有四五两个事 4、内存位置不同 成员变量:在堆中,跟着对象走 局部变量:在栈中,跟着方法走 5、生命周期不同 成员变量:随着对象的创建而产生,随着对象的消失而消失 局部变量:随着方法的调用而产生,随着方法的调用完毕而消失
封装思想
private隐藏 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1、问题: 定义的成员变量,可以拿来随意赋值,哪怕赋值不合理,解决方法呢: 将属性封装起来(隐藏细节) 关键字:private(私有化的) -> 被private修饰的成员只能在本类中使用,在别的类用不了 注意:将代码放到一个方法中,也是封装的体现 一个成员被private修饰也是封装的体现,只不过private最具代表性 private的使用: 修饰成员变量:private 数据类型 变量名 修饰方法:将public改为private,其他的都一样 这个时候属性被私有化了,外界调用不了,那么此时属性就不能直接赋值取值了,所以需要提供公共的接口 get/set方法 set方法:为属性赋值 get方法:获取属性值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.sjjws.method;public class Pervate { private String Name; private int age; public void setName (String xingMing) { Name = xingMing; } public String getName () { return Name; } public void Setage (int nianling) { if (nianling < 0 || nianling > 150 ) { System.out.println("错误年龄" ); } else { age = nianling; } } public int getAge () { return age; } }
小结:
用private将属性封装起来,外界不能直接调用,保护了属性
对外提供set\get方法,通过公共接口间接使用隐藏起来的属性
this的使用 1 2 3 4 5 6 7 如果成员变量和局部变量重名时,遵循就近原则,先找局部变量 this概述:代表的是当前变量 this可以区分重名的成员变量和局部变量 this点出来的是成员变量 this代表的是哪个对象呢? 哪个对象调用的this所在的方法,this就代表哪个对象,如下图,如果想要调用到成员变量,需要在选中的name前加一个this,这里的this代表的是Person对象,因为是在Person对象中调用的,所以指向person中的name
如果在person内打印this,这个时候打印出来的地址值是和person这个对象打印出来的地址值是一样的
构造方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 之前使用的new 类名() 这种形式就是一种构造方法,这个类名就是一种方法,记作类名只是为了好记 概述: 方法名和类名一致并且能初始化对象的方法 分类: 无参构造:没有参数 有参构造:有参数,参数是为指定的属性赋值 满参构造:给所有属性赋值 特点: 方法名和类名一致 没有返回值,连void都没有
无参构造 1 2 3 4 5 6 7 8 9 10 11 12 13 格式: public 类名(){ } 作用: new对象使用 特点: 每个类中默认有一个无参构造,不写也有,jvm会自动提供 使用: 之前使用的new 对象就是在调用构造方法
有参构造 1 2 3 4 5 6 7 8 9 10 11 格式: public 类名(参数){ } 作用: new对象 为属性赋值 注意: jvm不会自动提供有参构造,只能自己写出来,但是如果写上有参构造,那么jvm将不会提供无参构造,所有建议写有参构造的时候把无参构造一起写进去
JavaBean JavaBean是Java语言编写类的一种标准规范,符合 JavaBean
的类,要求:
类必须是具体的(非抽象abstract 和公共的 public class 类名)
并且具有无参数的构造方法,有参构造
成员变量私有化,并提供用来操作成员变量的 set 和 get 方法
开发的时候需要分包分层:
1 2 3 4 5 com.…….controller -> 专门放和页面打交道的类(表现层) com.…….service -> 专门放业务处理的类(业务层) com.…….dao -> 专门放和数据库打交道的类(持久层) com.…….pojo -> 专门放JavaBean类 com.…….utils -> 专门放工具类
1 2 快捷键: 快速生成标准JavaBean:Alt+insert
1 2 3 4 5 1、将来的JavaBean都是和数据库的表相关联的 类名 -> 表名 属性名 -> 列名 对象 -> 表中每行数据 属性值 -> 表中单元格的数据
这样的一个JavaBean类
在其他页面为其添加数据,可以直接调用有参构造
1 2 user user1 = new user(1,"tom","111"); user user1 = new user(2,"joker","admin");
JavaBean添加业务过程
查询业务过程
继承 static关键字 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static是一个静态关键字 使用: a.修饰一个成员变量 static 数据类型 变量名 b.修饰一个方法 修饰符 static 返回值类型 方法名(形参){ 方法体 return 结果 } 调用静态成员: 类名直接调用,不需要new对象 静态成员特点: a.静态成员属于类成员,不属于对象成员(非静态的成员属于对象成员) b.静态成员会随着类的加载而加载 c.静态成员优先于非静态成员存在于内存中 d.凡是根据静态成员所在的类创建出来的对象,都可以共享这个静态成员
静态成员在内存中的说明
static修饰成员的访问特点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 注意一下静态成员的特点,思考一下 1、在静态方法中能直接访问非静态成员吗? F 想要调用的时候使用new对象调用 2、在非静态方法中能直接访问静态成员吗? Y 3、在静态方法中能直接访问静态成员吗? Y 同类: 直接调用 类名调用(new对象调用) 不同类: 类名调用 4、在非静态方法中能直接访问非静态成员吗? Y 同类: 直接调用 类名调用 不同类: 类名调用
思考一
显然易见静态成员先加载,是不能直接调用的,但是可以另辟蹊径,这是访问自己的非静态,访问别人的非静态流程相同都是new对象
思考二
显然可以,后来者调用前者没啥毛病,直接调,如果是掉别人的静态方法,直接点出来,不用new对象
1 2 3 4 5 6 7 8 9 10 11 12 13 总结: 不管在不在同一个类当中,非静态成员都可以new对象调用 不管在不在同一个类当中,静态成员都可以类名调用 注意,不要将所有的成员都定义为静态成员 因为静态成员会随着类的加载而加载,如果将所有的成员都变为静态的,那么类一加载,所有的静态成员都会进入内存,占用大量空间 一般情况下,我们在抽取工具类的时候可以将工具类中的所有成员定义为静态的 那么什么时候定义工具类? 比如在写代码的时候,发现很多功能反复使用,那么就可以把这个功能定义
构建工具类时注意
构造方法使用private修饰,工具类中的成员都是静态的,静态成员都是类名调用,不需要new对象,所以工具类的构造方法都是用private修饰
如果构造方法被private修饰,那么在别的类中,就不能利用构造方法new对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.sjjws.stati;public class ArrayUtils { private ArrayUtils () {} public static int getMax (int [] arr) { int max = arr[0 ]; for (int i = 1 ; i < arr.length; i++) { if (max <= arr[i]) { max = arr[i]; } } return max; } }
定义可变参数 1 2 3 4 5 6 7 8 方法参数配置,只明确了参数的类型,但是不明确参数个数,此时就可以定义可变参数 定义格式: 数据类型...变量名 参数位置不能写多个可变参数,可变参数和其他参数一起使用时,可变参数要放在最后 可变参数的本质是一个数组
递归 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 概述: 方法内部自己调用自己; 分类: 直接递归: public static void method(){ method } 间接递归: A调B,B调C,C调A 注意: 递归必须要有出口,否则会出现“栈内存溢出” 就算有出口,也不要调用次数太多,使用return结束方法。
数组翻转 Java中并没有数组翻转的封装方法,需要自己敲一下
1 2 3 4 5 6 7 8 9 10 public static void main(String[] args) { int[] arr = {1,2,3,4,5,6,7} for (int min = 0, max = arr.length-1; min<max; min++ ; max--){ int temp = arr[min]; arr[min] = arr[max]; arr[max] = temp; } } // 左手导右手
==冒泡排序== 1 2 3 4 5 6 7 8 9 10 11 一种按照从小到大,或者从大到小的方法 for(int j = 1; j < arr.length; j++){ for(int i = 0; i < arr.length-j; i++){ if(arr[i]>arr[i+1]){ int temp = arr[i]; arr[i] = arr[i+1]; arr[i+1] = arr[i]; } } }
二分查找 1 2 3 4 前提:数组中的数据必须是有序的 查询思想: 老式查询:遍历数组,一个个比较 -> 查询效率比较慢 二分查找:每次找中间索引对应的元素进行比较查询(每一次查询少一半数据)
二分顾名思义,再加上数组中的数据是有序的,所以就很好理解
找中间索引
1 2 3 4 5 6 7 8 9 10 int min = 0; int max = arr.length-1; int mid = (min + max)/2; // 因为这个中间索引是需要不断变化的,留个容器 第一次二分没找到后,再找中间索引 min = mid + 1; 或者 max = mid - 1; mid = (min + max)/2;
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static int binary (int [] arr, int data) { int min = 0 ; int max = arr.length-1 ; int mid = 0 ; while (min<=max){ mid = (min+max)/2 ; if (data>arr[mid]){ min = mid + 1 ; } else if (data<arr[mid]){ max = mid - 1 ; } else { return mid; } } return -1 ; }
对象数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 person[] arr = new person [3 ]; person p1 = new person ("张三" ,18 ); person p2 = new person ("李四" ,18 ); person p3 = new person ("王五" ,18 ); arr[0 ] = p1; arr[1 ] = p2; arr[2 ] = p3;
方法参数 基本数据类型 方法进栈是有一个压栈动作的,越早进入的在栈底,方法运行完毕,会进行一个弹栈动作
看一下上面的代码,基本数据类型当作实参传递时,只会传递值,不会传递变量本身,method方法执行完毕后,再输出a和b的值,还是main方法中的a和b的值,并没有什么变化
引用数据类型 1 引用数据类型作为方法传递是会被影响的,因为传参传的是数组的地址值,是整个数组的内存空间,并不是基本数据类型的那种值,因此,传递引用数据类型,在方法内改变数据,到main方法中,数组也会改变,因为两个方法操作的是同一个数组
命令行参数 命令行参数是给main方法中的args传参
第一种方法
1 2 3 4 5 使用原始的命令行 javac 文件名.java Java 文件名 hello world // 通过这样的方式就给main传了两个参数,一个是hello,另一个是world
IDEA中也可以手动配置参数
debug调试 1 2 3 能清楚的看到每个变量在代码执行过程中的变化,便于寻找错误 使用: 点击出现断点
Java下 内容还是面向对象里的继承的内容
继承 1 2 3 4 5 6 7 8 9 10 11 定义了多个类,发现这些类有很多重复性代码,我们就定义了一个父类,将相同的代码抽取出来放到父类中,其他的类直接继承这个父类,就可以直接使用父类的内容了 如何继承: 子类 extends 父类 注意: 子类可以继承父类中私有和非私有成员,但是不能使用父类中私有成员 构造方法不能继承 继承不要从是否“拥有”方面来学习,要从是否能“使用方面来学习”
继承的使用 1 2 3 定义一个父类,写入重复使用的代码 定义一个子类 -> 继承 子类 extends 父类
继承中成员访问特点 成员变量 1 2 3 4 创建父类对象,可以使用父类的数据。但是不能使用子类的数据 总结: 看等号左边是谁,先调用谁中的成员,子类没有,找父类
父类对象不能调用子类对象的数据,子类可以继承调用父类非私有的数据,如果有重名的数据,子类先拿到自己的数据,如果自己没有才拿到父类的数据
这个思想在多态内也适用
这个时候拿父对象来new一个子,很迷的操作,我也不清楚,但是,从打印可以看到,打印了10000,是父类型里面的num,可以总结,先拿的数据是等号左边的相同类型
成员方法 以上是对成员变量的操作,下面是成员方法,方法与其相反,看new后面的是谁,就先调用谁的方法
方法的重写 1 2 3 4 5 6 7 8 9 概述:子类中有一个和父类方法名以及参数列表相同的方法 前提:继承 访问:看new的是谁,先调用谁中的,如果new的是子类,调用子类重写的方法,子类没有,找父类 检测是否有重写方法:在该方法上写 @Override 如果不报错说明是一个重写方法 使用场景: 功能升级改造,子类需要对父类已经实现好的功能进行重新改造
之前举例子访问成员方法时就涉及方法的重写
1 2 3 4 5 6 注意事项: 子类重写父类方法后,权限必须要保证大于等于父类权限 权限修饰符: public -> protected -> 默认(default) -> private 重写方法:方法名和参数列表要一样 私有方法不能被重写,构造方法不能被重写,静态方法static不能被重写
不写修饰符就是default,但是也不能写public权限最大
super和this new子类时,会首先默认执行一个父类无参构造
1 2 3 原因: 每个构造方法的第一行,都默认有一个super(),不写jvm自动提供 super()代表的是父类的无参构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 super()的具体使用 概述:代表的是父类引用 作用:可以调用父类中的成员 使用:既然是调用父类的东西,肯定就是在子类中调用了 1、调用父类构造方法 super() -> 调用无参构造 super(实参) -> 调用有参构造 2、调用父类成员变量 super.成员变量名 3、调用父类成员方法 super.成员方法名
感觉super和this类似,this指向当前对象,super指向继承的父对象
this的具体使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 this:代表的是当前对象 作用: 区分重名的成员变量和局部变量 调用当前对象中的成员 使用: 调用当前对象的构造:在构造中写 this() this(实参) 调用当前对象的成员变量: this.成员变量名 调用当前对象的成员方法 this.成员方法名
super和this都是写在第一行的,因此两者==不能同时出现==
继承的特点 1 2 3 4 5 6 7 8 9 10 11 继承只支持单继承,不能多继承(就是一个子类不能有多个父类) public class A extends B{} 但是可以多层继承 public class B extends C{} 一个父类可以有多个子类 构造方法不能继承,也不能重写 私有方法可以继承,但是不能被重写 静态方法可以继承,但是不能重写
之前说私有属性,可以继承但是不能直接使用,那么应该如何使用呢?
成员变量:
利用父对象的get/set方法
利用构造方法
1 2 3 4 在子类中定义构造方法: public zi(name,age){ super(name,age) }
这个super相当于调用了父对象的构造方法,原理就是子类的有参构造层层调用
抽象 将共有的方法抽取出来,形成父类,但是抽取出来的这个方法方法体没法确定,此时该方法不用写方法体了,没有方法体的方法,可以定义成抽象方法
1、抽象类怎么来的?抽取共性方法,放到父类中,发现方法没办法确定具体实现,因为每个子类对此方法的实现不一样,此时方法体无法确定,就可以定位成抽象方法
==抽象方法所在的类必须是抽象类==
继承到子类后再重写抽象方法
2、关键字:==abstract==
3、定义抽象方法
修饰符 abstract 返回值类型 方法名(参数);
4、抽象类:
public abstract class 类名{}
5、注意:
抽象方法所在的类一定是抽象类
抽象类中不一定非得有抽象方法
子类继承抽象父类时,需要重写父类中所有的抽象方法,不然编译报错
抽象类不能new对象,只能通过new子类对象去调用重写的方法
6、可以将抽象类看成是一类事物的标准,要求只要是属于这一个类的,都必须要拥有抽象类中的方法,必须要实现
注意事项: 1 2 3 4 5 1、抽象类不能直接new对象,只能创建非抽象子类的对象 2、抽象类中不一定非得有抽象方法,但是有抽象方法一定是抽象类 3、抽象类的子类必须重写父类中的所有抽象方法,除非子类也是抽象类 4、抽象类中可以有成员变量,构造方法,成员方法 5、抽象类中的构造方法不是为了new对象的,是供子类创建对象时,初始化父类属性使用的。使用需要利用super
接口 1、是一个引用数据类型,是一种标准、规则
2、关键字:
a.interface 接口
public interface 接口名{}
b.implements 实现
实现类 implements 接口名{}
3、接口中可以定义的成员:
a.jdk7以及之前:
抽象方法:public abstract -> 即使不写public abstract,默认也是抽象方法
成员变量:public static final 数据类型名 变量名 = 值 -> 即使不写,默认写
final是最终的,被final修饰的变量不能二次赋值,所有final修饰的变量也可以看作常量
b.jdk8
默认方法:public default 返回值类型 方法名(形参){}
静态方法:public static 返回值类型 方法名(形参){}
c.jdk9开始
私有方法:private(用的不多)
接口的使用
1 2 3 4 5 6 7 8 9 1、定义接口: public interface 接口名{} 2、实现: public class 实现类类名 implements 接口名{} 3、使用: a.实现类实现接口 b.重写接口中的抽象方法 c.创建实现类对象(接口不能直接new对象) d.调用重写方法
实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.sjjws.j_interface;public interface USB { public abstract void open () ; public abstract void close () ; } package com.sjjws.j_interface;public class Monse implements USB { @Override public void open () { System.out.println("打开" ); } @Override public void close () { System.out.println("关闭" ); } } package com.sjjws.j_interface;public class Test extends Monse { public static void main (String[] args) { Monse mouses = new Monse (); mouses.open(); mouses.close(); } }
接口中的成员 抽象方法 上方的示例代码就是对抽象方法的使用
默认方法 1 2 3 4 5 6 7 8 9 10 格式: public default 返回值类型 方法名(形参){ 方法体 return 结果 } 使用: a.实现定义类,实现接口 b.默认方法可以不重写 c.创建实现类对象,调用默认方法
默认方法直接调用即可,如果默认方法需要重写的话,需要将default去掉,default只能在接口中使用,去除default仍然是重写方法
静态方法 1 2 3 4 5 6 7 8 格式: public static 返回值类型 方法名(形参){ 方法体 return 结果 } 使用: 接口名直接调用
直接使用接口名.静态方法调用
静态方法和默认方法主要是用于临时添加一个小功能使用,因为如果添加为抽象方法,所有与接口有关的类都会报错,需要重写
成员变量 1 2 3 4 5 6 7 8 9 格式: public static final 数据类型 变量名 = 值 final是最终的,被final修饰的变量不能二次赋值,所以final修饰的变量也可以看作常量 特点: 不写public static final默认也有,调用和静态方法相同直接接口名调用 注意呢,变量必须要有值,因为是不能二次赋值的 习惯呢,将final修饰的变量名改为大写
接口的特点 1 2 3 4 5 6 7 8 9 1 、接口可以多继承 -> 一个接口可以继承多个接口 public interface InterfaceA extends InterfaceB ,InterfaceC{} 2 、接口可以多实现 -> 一个实现类可以实现一个或者多个接口 public class InterfaceImpl implements InterfaceA ,InterfaceB{} 3 、一个子类可以继承一个父类的同时实现一个或者多个结果 public class Zi exthends Fu implements InterfaceA ,InterfaceB{} 4 、注意 继承、实现接口,只要是父类中或者接口的抽象方法,子类或者实现类都需要重写
特殊情况:
当一个类实现多个接口时,如果接口中的抽象方法重名且变量名相同时,只需要重写一次
当一个类实现多个接口时,如果多个接口中默认方法有重名的,且参数一样的,必须重写一次默认方法
接口和抽象类的区别 1 2 3 4 5 6 7 8 9 相同点: a.都位于继承体系的顶端,用于被其他类实现或者继承 b.都不能new c.都包含抽象方法,其子类或者实现类都必须重写这些抽象方法 不同点: a.抽象类:一般作为父类使用,可以有成员变量,构造,成员方法,抽象方法等 b.接口:成员单一,一般抽取接口,抽取的都是方法,视为功能的大集合 c.类不能多继承,但接口可以
接口内一般定义只有抽象方法,偏向于功能的大集合,抽象类内什么都可以有
多态 不要从字面意思上来理解多态,要从使用形式上来掌握
要知道多态的好处和前提
1 2 3 4 5 6 7 8 前提: 1、必须有子父类继承或者接口实现关系 2、必须有方法的重写(没有重写,多态没有意义),多态玩的就是重写方法 3、new对象:父类引用指向子类对象 之前也这么使用过一次 Fu fu = new zi() -> 大致理解为大类型接收了应该小类型数据 注意: 多态下不能直接调用子类特有功能
回忆一下
理解一下
多态下成员访问的特点 成员访问特点已经回忆完了
成员变量:
==看等号左边是谁,先调用谁中的变量==
成员方法:
==看new的是谁,先带哦有谁中的成员方法==
多态的好处 1 2 3 4 5 6 7 8 9 10 11 问题: 如果使用原始方式new对象,既能调用重写的,还能调用继承的,还能调用自己特有的成员,但是多态方式new对象,只能调用重写的,不能直接调用子类特有的成员,那么为什么还要用多态 多态和原始方式的优缺点: 原始方式: 优点:既能调用重写的,还能调用继承的,还能调用自己特有的成员 缺点:扩展性差(主要是方法参数传递方面,会加大代码量) 多态形式: 优点:扩展性强 缺点:不能直接调用子类特有功能
说一下我自己的感觉,多态的这个优点适用于同一个给方法很多人都在用,但是只有几个数不一样,是不是感觉很熟悉,微信小程序中的idx,解决大量重复代码的
形参传递父类类型,调用此方法父类类型可以接收任意他的子类对象
传递哪个子类对象,就指向哪个子类对象,就调用哪个子类对象重写的方法
1 2 3 4 5 6 7 8 9 zi zi = new zi ()method(zi) public static void method (fu fu) { fu.eat(); }
多态中的转型 哎,就是想用多态调用子类特有方法怎么办?
向上转型 1 2 3 就好比是:double b = 1; 表现方式: 父类类型 对象名1 = new 子类对象()
向下转型 1 2 3 好比是:int i = (int)b 强转 表现方式: 子类类型 对象名2 = (子类类型)对象名1
想要调用子类特有功能就需要用到向下转型了
1 2 3 fu name1 = new zi ();zi name2 = (zi)name1name2.xxx(特有功能)
向下转型问题 这个时候呢,由于强转是会出现一些问题的:如
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test02 { public static void main (String[] args) { zii zii = new zii (); method(zii); } public static void method (fu fu) { fu.eat(); zi zi = (zi) fu; zi.work(); } }
这段代码运行起来,会出现类型转换异常(ClassCastException)
当调用method时,传递zii对象,然后由fu向上转换成fu类型,但是方法中,将代表zii对象的fu强转成了zi
此时等号左右两边 不一样,所有出现了类型转换异常
解决方法也很简单,在向下转型之前,判断一下类型即可
1 2 3 4 5 使用关键字:instanceof 判断结果为布尔型 使用: 对象名 instanceof 类型 判断的是关键字前面的对象是否符合关键字后面的类型
1 2 3 4 5 6 7 8 if (fu instanceof zi){ zi zi = (zi) fu; zi.work(); } if (fu instanceof zii){ zii zii = (zii) fu; zii.eat(); }
杂项 权限修饰符 Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限
public:公共的,最高权限,被public修饰的成员,在哪里都能访问
protected:受保护的
default:默认的 注意 不写权限修饰符就是默认权限,不能直接将default写出来,只有在接口中可以用default
private:私有的,只能在自己的类中直接访问
只需要知道一个成员被这四个权限修饰符修饰在四种情况下能不能访问就行了
public
protected
default
private
同类
yes
yes
yes
yes
同包不同类
yes
yes
yes
no
不同包子父类
yes
yes
no
no
不同包非子父类
yes
no
no
no
public具有最大权限,private具有最小权限
如果没有特殊考虑的话
1 2 3 1、属性:用private -> 封装思想 2、成员方法:用public -> 便于调用 3、构造public -> 便于new对象
final关键字 1 2 3 4 5 6 7 8 9 final:最终的 使用: 修饰一个类 修饰一个方法 修饰一个局部变量 修饰一个成员变量 修饰一个对象 只需要知道被final修饰之后的特点即可
修饰类 1 2 3 4 5 格式: public final class 类名{} 特点: 太监类,不能被继承,没后代
修饰方法 1 2 3 4 5 6 7 8 9 10 11 格式: 修饰符 final 返回值类型 方法名(形参){ 方法体 return 结果 } 特点: 被final修饰的方法,不能被重写 注意: final和abstract两个关键字冲突不能同时使用
修饰局部变量 1 2 3 4 5 6 7 8 9 10 11 12 格式: final 数据类型 变量名 = 值 特点: 被final修饰的变量不能二次赋值 final int i = 10; && final int j; j = 100; 这都属于一次赋值
修饰对象 1 2 3 4 5 格式: final 数据类型 对象名 = new 对象() 特点: 被final修饰的对象,地址值不能改变。但是对象中的属性值可以改变
举个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main (String[] args) { person p1 = new person ("张三" ,18 ) System.out.println(p1); p1 = new person ("李四" ,10 ) System.out.println(p1); } final person p1 = new person ("张三" ,18 ) System.out.println(p1); p1 = new person ("李四" ,10 ) System.out.println(p1); final person p1 = new person ("张三" ,18 ) System.out.println(p1); p1.setName("李四" ) p1.setAge(10 )
修饰成员变量 1 2 3 4 5 6 格式: final 数据类型 变量名 = 值 特点: 1、需要手动赋值 2、不能二次赋值
代码块 构造代码块 1 2 3 4 5 6 7 8 格式: { 代码 } 特点: 构造代码块优先于无参构造方法执行 每new一个执行一次
静态代码块 1 2 3 4 5 6 7 8 格式: static{ 代码 } 特点: 静态代码块优先于构造代码块执行 只执行一次
静态代码块的使用更加广泛,他常用于需要有限执行,但是只需要执行一次的情况
小小的拓展一下
Java操作数据库
1 2 3 4 1、注册驱动 2、初始化操作数据库的地址 3、初始化数据库用户名 4、初始化数据库密码
这些东西都是需要放到静态代码块内的
内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 什么时候使用内部类: 当一个事物内部,还有一个部分需要完整的结构去描述,而这个内部的完整结构又只为外部事物提供服务,那么整个内部的完成结构最好使用内部类 比如:组织-器官-个体,人体本事具有一定属性,需要去描述,人体内部的一些器官也有一定的特殊属性和行为,这个时候,器官就可以看作人体中的一个内部类 在Java中允许定义位于另外一个类内部,前者就称之为内部类,后者称之为外部类 class A{ class B{ } } 类A就是类B的外部类 类B就是类A的内部类 分类: 成员内部类(静态、非静态) 局部内部类 匿名内部类(重点)
静态成员内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 格式: 直接在定义内部类的时候加上static关键字 public class A{ static class B{ } } 注意: a.内部类可以定义属性,方法,构造等 b.静态内部类可以被final或者abstract修饰 被final修饰之后,不能被继承 被abstract修饰之后,不能new c.静态内部类不能调用外部的非静态成员 d.内部类还可以被四种权限修饰符修饰 调用静态内部类成员: 外部类.内部类 对象名 = new 外部类.内部类()
非静态成员内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 格式: public class A{ class B{ } } 注意: a.内部类可以定义属性,方法,构造等 b.静态内部类可以被final或者abstract修饰 被final修饰之后,不能被继承 被abstract修饰之后,不能new c.静态内部类不能调用外部的非静态成员 d.内部类还可以被四种权限修饰符修饰 注意点和静态成员内部相同,但是调用方法不同: 外部类.内部类 对象名 = new 外部类().new 内部类()
有一个问题:
外部类的成员变量和内部类的成员变量以及内部类的局部变量重名时,如何区分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Test { String name = "张三" ; class monse { String name = "李四" ; public void person () { String name = "王五" ; System.out.println(name); System.out.println(this .name); System.out.println(Test.this .name); } } }
局部内部类 局部内部类基本操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 可以定义在方法中,代码块中,构造中 局部内部类使用起来很麻烦,演示一下 public class person { public void eat () { class hemp { public void jump () { System.out.println("你好" ) } } new hemp ().jump } }
局部内部类实际操作 补充一下不同类作为方法参数和返回值的情况
接口类型作为方法参数传递和返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 接口不能直接new 对象,在多态部分提到过,接口作为方法参数传递时,传递的是他的实现类 public class Test { public static void main (String[] args) { monse monse = new monse (); method(monse); USB usb = method01(); } public static void method (USB usb) { usb.open() } public static USB method01 () { return new mouse (); } }
总结:
接口作为方法参数,传递实参时,传递的是实现类对象
接口作为返回值类型时,实际返回的也是实现类对象
抽象类作为方法参数和返回值
抽象类作为方法参数传递时,传递的是其子类对象
抽象类作为方法返回值类型返回,实际返回的是其子类对象
普通类作为方法参数和返回值
普通类作为方法参数和返回值时,传递的是对象
局部内部类实际操作
拿接口示范一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Test { public static void main (String[] args) { USB usb = method(); usb.open(); } public static USB method () { class Monse implements USB { @Override public void open () { System.out.println("鼠标打开" ) } } return new Monse (); } }
匿名内部类(重点)
所谓的匿名内部类,可以理解为没有显示声明出类名的内部类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 要想实现接口,或者抽象类的方法需要以下步骤 1、创建实现类,实现接口 2、重写方法 3、创建实现类对象 4、调用方法 如果呢,只想使用一次接口中的方法,那么麻烦大可不必,可以四合一 格式: new 接口/抽象类(){ 重写方法 }.重写的方法(); --------or------- 类名 对象名 = new 接口/抽象类() { 重写方法 } 对象名.重写的方法(); // 注意这里是对象名而不是类名,如果定义了类名就是实现类,不是匿名内部类了,感觉类似于python中的一行函数
匿名内部类在代码编译时会生成class文件的
简单调用一次接口中的方法时,就可以使用匿名内部类
将一种格式代表实现类对象或者子类对象来看待
匿名内部类的复杂使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class TEST01 { public static void main (String[] args) { method(new USB () { @Override public void open () { System.out.println("hello" ); }; }); } public static void method (USB usb) { usb.open(); } }
同样的返回值也可以这样操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class TEST01 { public static void main (String[] args) { USB usb = method(); } public static USB method () { return new USB (){ @Override public void open () { System.out.println("hello" ); }; } } }
异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public calss test { pubilc static void main (String[] args) { method(); int [] arr1 = new int [3 ]; System.out.println(arr1[4 ]); SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); String time = "2000-10-10 10:10:10" ; Date date = sdf.parse(time); System.out.printlin(date); } public static void method () { method(); } }
创建异常对象(了解)
创建异常对象,只是为了后面学习如何处理异常,其他的暂时没有啥意义
1 2 3 关键字:throw 格式: throw new 异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Test { public static void main (String[] args) { String s = "a.txt" ; method(s); } public static void method (String s) { if (!s.endsWith(".txt" )){ throw new NullPointerException (); } System.out.println("执行" ) } }
异常处理 throws 1 2 3 4 5 6 7 8 9 格式: 在方法参数和方法体之间写 throws 异常 public static void 方法名(方法参数)throws 异常{ 方法体 } 作用方面比较鸡肋,是将异常信息向上抛,而jvm处理异常的逻辑也是往上抛,如果没人处理,最后再由jvm来打印异常信息,终止程序
上抛多个异常: throws 异常1,异常2
如果多个异常之间有子父类关系,可以直接throws父类异常
也可以擒贼擒王,直接throws Exception
层层上抛,没啥意思
try……catch 1 2 3 4 5 6 格式: try { 可能出现异常的代码 }catch(异常 对象名){ 处理异常的代码 -> 正常开发是将异常信息保存到日志文件中 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.io.FileNotFoundException;public class test { public static void main (String[] args) { String s = "a.txtl" ; try { method(s); } catch (FileNotFoundException e) { System.out.println(e); e.printStackTrace(); } } public static void method (String s) throws FileNotFoundException { if (!s.endsWith(".txt" )) { throw new FileNotFoundException ("找不到文件" ); } System.out.println("执行" ); } }
处理多个异常 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 格式: try { 可能出现异常的代码 }catch(异常 对象名){ 处理异常的代码 }catch(异常 对象名){ 处理异常的代码 }catch(异常 对象名){ 处理异常的代码 }catch(异常 对象名){ 处理异常的代码 }catch(异常 对象名){ 处理异常的代码 } 和throws一样,如果多个异常之间有子父类关系,可以直接catch父类异常
finally关键字 1 2 3 4 5 6 7 8 9 10 11 12 13 概述:代表的是不管是否触发异常,都会执行的代码块 特殊情况:如果之前执行了System.exit(0) -> 终止jvm虚拟机 需要搭配try……catch使用 格式: try { 可能出现异常的代码 }catch(异常 对象名){ 需要处理的异常 }finally{ 必须执行的代码 }
finally里有一个比较细的点,执行顺序
1 2 3 4 5 6 7 8 9 10 11 12 public static int method () { try { String s = null ; System.out.println(s.length()); return 1 ; }catch (Exception e){ return 2 ; }finally { System.out.println("执行" ); } }
这里控制台会先输出 执行
然后输入返回值2,如果没有只是 return 3
的话会直接截胡,输出 执行 和 3
finally的使用场景
1、关闭资源
2、原因,对象如果没有用了,GC(垃圾回收器)回收,用来回收堆内存中的垃圾,释放内存,但是有一些对象GC回收不了,比如:连接对象(Connection),IO流对象,Socker对象,这些对象GC回收不了,因此需要手动回收关闭。
将来不能回收的对象new完之后,后续不管是否操作成功,是否有异常,我们就手动关闭,这个时候就需要将关闭资源的代码放到finally中
抛异常的注意事项 1 2 3 4 1、如果父类方法抛了异常,那么子类重写后需不要抛 可抛可不抛 2、如果父类方法中没有抛异常,那么子类重写可不可以抛异常 不能抛
try throws使用时机,三层架构
1、编译时期异常必须要处理,不然没法往下写了
2、运行时期异常一般不处理,一旦出现运行时期异常,肯定是代码有问题,try catch没有意义,直接修改代码细节即可
自定义异常 自定义异常,首先要有这个异常类
定义异常类时需要继承
如果继承Exception就是编译时期异常
如果继承RuntimeException就是运行时期异常
如果要传入异常原因的话,只需要在自定义异常写一个有参构造
1 2 3 public LoginUserException (String message) { super (message) }
打印错误信息的三个方法 1 2 3 4 都是Throwable类中的方法: String toString(); // 输出异常类型和设置的异常信息 String getMessage(); // 输出设置的异常信息 void printStackTrace(); // 打印异常信息是最全的:包括异常类型,信息,以及出现的行数等
Object类 继承来继承去,最后继承的都是Object,所有类都会直接或者间接的继承Object,Object类是根类
这里的Object指的是lang包内的Object
ctrl+n 搜索
toString()
1 2 3 4 5 6 7 8 9 10 11 toString方法并不复杂 public String toString () { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } 注意: 1 、如果没有重写Object中的toStrig方法,直接输出对象名会默认输出Objict中的toString方法,直接输出地址值 2 、如果重写了object中的toString,再输出地址值,重写没意义,所以重写完tostring之后,应该返回对象的内容
之前在学生管理系统的编写上,就使用过重写toString,toString也可以通过 ait+insert 快捷键快速生成
1 2 3 4 5 6 7 ArrayList<String> list = new ArrayList <>(); list.add("张三" ); list.add("李四" ); list.add("王五" ); System.out.println(list); System.out.println(list.toString());
这个是写好的toString()重写
equals
1 2 3 4 5 6 作用: 比较两个地址值是否相等 public boolean equals (Object obj) { return (this == obj); }
1 2 3 4 5 这里有两种方法 第一个是直接p1==p2 第二种是 p1.equals(p2); 将p2作为方法参数传入equals方法,然后this和obj比较,由于是p1调用的equals,所以this是指向p1的
1 2 3 == 针对于基本数据类型来说,比较的是值 对两个基本数据类型使用equals,调用的是重写后的equals,并不是object中的equals == 针对于引用数据类型来说,比较的是地址值
如果没有重写object中的equals方法,那么就会调用object中的equals方法,如果重写了就调用重写后的equals方法
自己重写一个equals
考虑三个因素,空、自身、目的(比较内部信息)
这样比较好看,也可以直接根据引导生成重写后的equals,意思和上方代码是一样的
1 2 3 4 5 6 7 8 9 @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; object object = (object) o; return age == object.age && Objects.equals(name, object.name); }
小结:
1、如果直接输出对象名,不想输出地址值,重写toString方法
2、如果想要比较两个对象的内容,就重写运行equals方法
3、输出对象名找toString,比较对象找equals
4、ait+insert 快捷键重写方法
clone方法(克隆) 1 2 3 4 5 6 作用: 复制一个属性值一样的新对象 使用: 需要被克隆的对象实现Cloneable接口 重写clone方法
截图少截了,重写clone之前,需要实现Cloneable接口
1 public class object implements Cloneable
经典接口 java.lang.Comparable
基本数据类型(除布尔类型外)需要比较大小的话,直接使用比较运算符即可,但是引用数据类型是不能直接使用比较运算符来比较大小的。那么如何解决这个问题?
java给所有引用数据类型的大小比较,指定了一个标准接口,就是java.lang.Comparable接口
1 2 3 4 5 package java.lang;public interface Comparable { int compareTo (Object obj) ; }
如果想要使得我们某个类的对象可以比较大小,怎么做?
1 2 3 4 5 6 7 第一: 哪个类的对象要比较大小,哪个类就实现Comparable接口,并重写方法 方法体就是你要如何比较当前对象和指定的另一个对象的大小 第二: 对象比较大小时,通过对象调用compareto方法,根据方法的返回值决定谁打谁小。 this对象(调用compareTo)减 指定对象(传入compareTo()的参数对象)大于0,返回正整数 this对象(调用compareTo)减 指定对象(传入compareTo()的参数对象)小于0,返回负整数 this对象(调用compareTo)减 指定对象(传入compareTo()的参数对象)等于0,返回零
代码实例:
代码排序还是依靠冒泡排序,只是使用compareTo()方法进行比较的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.sjjws.a_comparable;public class Test { public static void main (String[] args) { student[] students = new student [3 ]; students[0 ] = new student ("张三" ,100 ); students[1 ] = new student ("李四" ,60 ); students[2 ] = new student ("王五" ,80 ); for (int j = 0 ; j <students.length-1 ; j++) { for (int i = 0 ; i <students.length-1 -j ; i++) { if (students[i].compareTo(students[i+1 ])>0 ){ student temp = students[i]; students[i] = students[i+1 ]; students[i+1 ] = temp; } } } for (int i = 0 ; i <students.length ; i++) { System.out.println(students[i]); } } }
实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.sjjws.a_comparable;public class student implements Comparable { String name; int score; public student () { } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getScore () { return score; } public void setScore (int score) { this .score = score; } @Override public String toString () { return "student{" + "name='" + name + '\'' + ", score=" + score + '}' ; } public student (String name,int score ) { this .name = name; this .score = score; } @Override public int compareTo (Object o) { student s1 = (student) o; return this .getScore() - o.getScore(); } }
java.util.Comparator
思考:
1、如果一个类,没有实现Comparable接口,而这个类又不方便修改(例如:一些第三方的类,你只有.class文件,没有源文件)
2、如果一个类,实现了Comparable接口,也指定了两个对象比较大小的规则,但是此时此刻我不想按照它预定义的方法比较大小,但是我又不能随意修改,因为会影响其他地方的使用,怎么办?
这个时候就需要用到Comparator接口
1 2 3 4 5 package java.util;public interface Comparator { int compare (Object o1,Object o2) ; }
那么我们想要比较某个类的两个对象的大小,怎么做呢?
1 2 3 4 5 6 7 第一步: 编写一个类,我们称之为比较器类型,实现java.util.Comparator接口,并重写方法 方法体就是你要如何指定的两个对象的大小 第二步:比较大小时,通过比较器类型的对象调用compare()方法,将要比较大小的两个对象作为compare方法的实参传入,根据方法的返回值决定谁大谁小。 o1对象减o2大于0返回正整数 o1对象减o2小于0返回负整数 o1对象减o2等于0返回零
实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import java.util.Comparator;public class student01 implements Comparator { String name; int score; public student01 () { } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getScore () { return score; } public void setScore (int score) { this .score = score; } @Override public String toString () { return "student{" + "name='" + name + '\'' + ", score=" + score + '}' ; } public student01 (String name, int score ) { this .name = name; this .score = score; } @Override public int compare (Object o1, Object o2) { student01 s1 = (student01) o1; student01 s2 = (student01) o2; return s1.getScore()-s2.getScore(); } }
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Test { public static void main (String[] args) { student01[] students = new student01 [3 ]; students[0 ] = new student01 ("张三" ,100 ); students[1 ] = new student01 ("李四" ,60 ); students[2 ] = new student01 ("王五" ,80 ); student01 student01 = new student01 (); for (int j = 0 ; j <students.length-1 ; j++) { for (int i = 0 ; i <students.length-1 -j ; i++) { if (student01.compare(students[i],students[i+1 ])>0 ){ student01 temp = students[i]; students[i] = students[i+1 ]; students[i+1 ] = temp; } } } for (int i = 0 ; i <students.length ; i++) { System.out.println(students[i]); } } }
API部分 String 了解string string是lang包下的类,lang包下的类在使用的时候,不需要导包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 String类代表字符串 特点: 1、Java程序中的所有字符串面值(如"abc")都作为此类的实例(对象)实现 凡是带双引号的,都是String的对象 String s = "abc" "abc"就是对象;String就是对象的数据类型;s就是对象名 2、字符串是常量,它们的值在创建后不能更改 String s = "hello"; s += "world"; 这种操作虽然存在,但底层的逻辑是产生一个新的对象的,它的地址值改变了,并不是修改了s,字符串是常量,在底层代码内是使用final修饰的 3、String对象是不可变的,所以可以共享 String s1 = "abc"; String s2 = "abc"; System.out.println(s1==s2) S1和s2的地址值是一样的,返回结果为true
共享:指向的堆内存空间相同
想到了一个很有意思的事情
学习equals是曾经比较过两个字符串,返回是false,如下图
解释一下:
常量池:
首先要引入常量池这个概念,当使用自负床字面量创建字符串时 如: String s1 = “abc”; Java会在常量池内查找是否已经存在相同内容的字符串。如果存在,则直接返回该字符串的引用;如果不存在,则在常量池中创建一个新的字符串并返回其引用。这个机制也很好理解,减少内存栈资源占用。
new关键字:
当使用new关键字创建字符串时,Java会在堆内存中创建一个新的字符串对象,即使字符串常量池中已经存在相同内容的字符串
然后就很好理解了,s1和s2的地址值是相同的,但是s2,s3,s4的地址值各不相同
值得注意的是,new的对象的内容是abc在常量池中的地址值,如果abc没有在常量池内,那么new的话会先在常量池中创建一个abc,然后将常量池中的abc的地址值给到 new 出的对象
String的实现原理 1 2 3 4 5 6 7 1、jdk8的时候:String底层是一个被final修饰的char数组 -> private final char[] value; 2、jdk9开始到以后:底层是一个被final修饰的byte数组 -> private final byte[] value; jdk8 - jdk9 版本更新出现了拉姆达表达式(函数式编程思想),因此有了很大变动,所有有些脚本、软件会要求Java的jdk8环境,其他的版本更新主要是优化内存 一个char类型数据占两个字节,一个byte类型数据占一个字节,节省了内存空间
1 字符串定义完之后,数组就创建好了,被final一修饰,数组的地址值就固定死了
String的创建 1 2 3 4 5 6 7 1 、String() -> 利用String的无参构造创建String对象2 、String(String original) -> 根据字符串创建String对象3 、String(char [] value) -> 根据char 数组创建String对象4 、String(byte [] bytes) -> 通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String 简化形式: String s = "" ;
演示一下:注意char数组不能用双引号
再说一下byte数组的事
-> 通过平台的默认字符集解码指定的 byte 数组,构造一个新的 String
平台:操作系统
操作系统默认字符集:GBK
GBK这个东西前面说过
GBK:一个中文占2个字节
UTF-8:一个中文占3个字节
中文在编码集中一般为负数,但是使用byte的时候注意,要在编码表中可以找到,否则就是乱码
这里的“你”字是由三个字节的,原因是代码是在idea中写的,idea启动的时候,会自动加一个启动参数,此启动参数为UTF-8 -Dfile,encoding=UTF-8
以上是比较常用的几种构造,现在看几个不常用的
1 2 3 4 5 6 7 8 9 1 、String(char [] value, int offset, int count) -> 将char 数组的一部分转化成String对象 value:要转String的char 数组 offset:从数组的哪个索引开始转 count:转多少个 2 、String(byte [] bytes, int offset, int length) -> 将byte 数组的一部分转成String对象 bytes:要转String的byte 数组 offset:从数组的哪个索引开始转 length:转多少个
1 2 3 4 5 6 7 问题: String s = new String("abc"); 共有几个对象 两个 new 和 "abc" String s = new String("abc"); 共创建了几个对象 一个或者两个 要看 "abc" 有没有提前创建,如果之前没有就会创建两个,先创建常量池中的 abc 再创建 new 对象,因为new对象的是abc在常量池中的地址值
1 2 3 4 5 6 7 8 9 String s1 = "hello" ; String s2 = "world" ; String s3 = "helloworld" ; String s4 = "hello" + "world" ; String s5 = s1 + "world" ; String s6 = s1 + s2; System.out.println(s3 == s4); System.out.println(s3 == s5); System.out.println(s3 == s6);
以上代码的输出结果分别为 true false false
为了能够可视化的看到区别呢,倒置了一个小时的反编译,解决了一个小问题 jd-gui 无法打开class文件时,将文件打成压缩包再放到jd-gui中,但是编译出来的情况和网课不一样
抛开上面不看了,这是XJad编译出来的
从反编译结果可以看出s3和s4是等价的,但是s5、s6是new了新的对象的,因此地址值不同
总结一下这个问题:
1、字符串拼接,如果等号右边是字符串字面值拼接,不会产生新的对象
2、字符串拼接,如果等号右边有变量参数拼接,会产生新字符串对象
String常用方法 判断方法 1 2 3 4 5 6 7 boolean equals(String s) -> 比较字符串内容 boolean equalsIgnoreCase(String s) -> 比较字符串内容,忽略大小写(可用于图片验证码) equals方法还是一样 s1.equals(s2) s1.equalsIgnoreCase(s2)
Java中也有比较两个对象的方法
工具类:Objects(注意是Objects不是根类Object)
方法:equals,这个是重写的equals方法,但是是传的两个对象,而且有防空指针的效果
1 2 3 public static boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b)) }
获取功能 1 2 3 4 5 6 7 8 9 10 11 12 1、获取字符串长度 int length(); 2、字符串拼接,返回新字符串 String concat(String s); 3、根据索引获取对应字符 char charAt(int index); 4、获取指定字符在大字符串中第一次出现的索引位置 int indexOf(String s); 5、截取字符串,从指定索引开始截取到最后,返回新字符串 String subString(int beginIndex); 6、截取字符串,从beginIndex开始到endIndex结束(含头不含尾),返回新字符串 String subString(int beginIndex, int endIndex);
这个时候就可以遍历一下字符串了
1 2 3 4 String s = "asdfghjkl"; for (int i = 0; i < s.length()-1; i++) { System.out.println(s.charAt(i)); }
转换功能 1 2 3 4 1、char[] toCharArray() -> 将字符串转为char数组 2、byte[] getBytes() -> 将字符串转成byte数组 3、String replace(CharSequence c1, CharSequence c2) -> 替换字符 4、byte[] getBytes(String charsetName) -> 按照指定的编码将字符串转成byte数组
CharSequence是String类的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 实例代码: System.out.println("------------------------------------" ); char [] chars = s1.toCharArray(); for (int i = 0 ; i < chars.length; i++) { System.out.print(chars[i]); } System.out.println(); byte [] bytes = s1.getBytes(); for (int i = 0 ; i < bytes.length; i++) { System.out.print(bytes[i]+"," ); } System.out.println(); String s2 = s1.replace("asd" ,"qwe" ); System.out.println(s1+"||" +s2); byte [] byteGBK = "你好" .getBytes("GBK" ); for (int i = 0 ; i < byteGBK.length; i++) { System.out.print(byteGBK[i]); }
分割功能 1 2 3 String[] split(String regex) -> 按照指定的规则分割字符串 注意:regex写的是正则表达式 -> .在正则表达式中代表任意字符,如果要以.来切割的话,使用转义符//
其他方法 1 2 3 4 5 6 1、boolean contains(String s) -> 判断老字符串内是否包含指定的字符串 2、boolean endsWith(String s) -> 判断老字符串是否以指定的字符串结尾 3、boolean startsWith(String s) -> 判断老字符串是否以指定的字符串开头 4、String toLowerCase() -> 将字母转成小写 5、String toUpperCase() -> 将字母转成大写 6、String trim() -> 去掉字符串两端空格
StringBuilder
概述:一个可变的字符序列,此类提供了一个与StringBuffer兼容的一套API,但是不保证同步(线程不安全,效率高)
作用:主要是字符串拼接
问题:
1、String也能做到字符串拼接,直接使用+拼接,那么为什么还要用StringBuilder拼接呢
2、原因:
String每拼接一次,就会产生新的字符串对象,就会在堆内存中开辟新空间,如果拼接过多,会占用内存,效率降低
3、StringBuilder,底层自带一个缓冲区(没有被final修饰的byte数组)拼接字符串之后都会在缓冲区中保存,在拼接过程中,不会随意产生新对象,节省内存
特点:
1、底层自带缓冲区,此缓冲区是备用被final修饰的byte数组,默认长度是16
2、如果超出了数组长度,数组会自动扩容
因为定长,所以创建一个新长度的数组,将老数组的元素复制到新数组中,然后将新数组的地址值重新赋值给老数组
3、默认每次扩容老数组的2倍+2
如果一次性添加的数据超出了默认的扩容数组长度(2倍+2)。但有例外,比如存了36个字符,超出了第一扩容的34,就按照实际数据个数为准,就是以36扩容
深入♂了解一下 看一下StringBuilder的底层代码
创建出来的缓冲区
搞点事情,打个断点观察一下
1 sb.append("1111111111111111111111111111111");
判断是否应该扩容
开始扩容
返回34之后在copyOf内创建了新的长度为34的数组,赋值,更改地址等一系列操作
补充:
如果len大于34的话,最后的扩容计算会正好等于length,然后返回这个值
值得注意的是,StringBuilder扩容时也会创建新的对象,也会占用内存,但是不可否认的是,完成同样的事,StringBuilder所占的内存更少
StringBuilter的使用 1 2 3 4 5 构造: StringBuilder() StringBuilder(String str) 两种构造方法,空参是空的缓冲区,传参是往缓冲区添加
1 2 3 4 5 常用方法: StringBUilder append(任意类型数据) -> 字符串拼接的效果,但是底层逻辑和字符串拼接不一样,返回值是StringBuilder自己 StringBuilder reverse() -> 字符串翻转,返回的是StringBUilder自己 String toString() -> 将StringBuilder转换成String。老朋友toString了 用StringBuilder拼接字符串是为了效率,为了不占内存,那么拼接完成之后处理字符串就需要调用String中的方法,所以需要将StringBuilder转换成String
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class SB { public static void main (String[] args) { StringBuilder sb = new StringBuilder (); StringBuilder s1 = sb.append("张三" ); System.out.println(sb==s1); sb.append("李四" ).append("王五" ); System.out.println(sb+"---||---" +sb.reverse()); String s2 = sb.toString(); } }
小练习:判断键盘录入的内容是否回文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class SB { public static void main (String[] args) { StringBuilder sb = new StringBuilder (); Scanner sc = new Scanner (System.in); System.out.print("请输入要判断的内容:" ); String oldster = sc.next(); sb.append(oldster); String newS = sb.reverse().toString(); if (oldster.equals(newS)) { System.out.println("符合回文特征" ); } else { System.out.println("不符合回文特征" ); } } }
总结一下:
String:拼接字符串效率低,每拼接一次,都会产生一个新的字符串对象,耗费内存资源
StringBuilder和StringBuffer区别:
1、相同点:
用法一样、作用一样
2、不同点
StringBuilder拼接效率比StringBuffer高,但是线程不安全
StringBuffer反之
拼接效率比较:StringBuilder>StringBuffer>String
数学相关类 Math 1 2 3 4 5 6 7 概述:数学工具类,主要用于数学运算 特点: 1、构造方法私有,外界不能根据构造方法new对象 2、方法都是静态 使用: 类名直接调用
Math方法 1 2 3 4 5 6 static int abs(int a) -> 求参数的绝对值 static double ceil(double a) -> 向上取证 static double floor(double a) -> 向下取证 static long round(double a) -> 四舍五入 static int max(int a, int b) -> 求两个数之间的较大值 static int min(int a, int b) -> 求两个数之间的较小值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class math { public static void main (String[] args) { System.out.println(Math.abs(-10 )); System.out.println(Math.ceil(3.6 )); System.out.println(Math.floor(3.6 )); System.out.println(Math.round(3.6 )); System.out.println(Math.round(-2.8 )); System.out.println(Math.max(10 , 20 )); System.out.println(Math.min(10 , 20 )); } }
Math类还包含用于执行基本数学运算的方法,如初等函数、对数、平方根和三角函数
BigInteger 1 2 3 4 5 6 7 8 9 10 11 12 13 将来可能操作特别大的数据,大到比long还大,这种数据我们称之为“对象” 作用: 处理超大整数 构造: BigInteger(String val) -> 参数的格式必须是整数 方法: BigInteger add(BigInteger val) -> 加 返回其值为(this + val)的BigInteger BigInteger subtract(BigInteger val) -> 减 返回值为(this - val)的BigInteger BigInteger multiply(BigInteger val) -> 乘 返回其值为(this * val)的BigInteger BigInteger divide(BigInteger val) -> 除 返回其值为(this / val)的BigInteger
BigInteger主要是处理大数据,数据大到基本数据类型接受不了,传入字符串的形式,进行运算
int intValue() 将BigInteger转成int
long longValue() 将BigInteger转成long
BigInteger上限:42亿的21亿次方,这个大小的数一般内存遭不住,所以也可以认为BigInteger无上限
BigDecimal 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 之前说过不能直接用floor或者double,因为会出现精度损失的问题 这个BigDecimal 主要就是解决floor和double直接做运算时出现的精度损失问题 构造: BigDecimal(String val) -> val必须说数字形式 // 构造方式有很多种,也可以直接传入double类型,但是结果具有不可预知性,不准确 // 如果死犟的话,可以使用静态方法 static valueOf(double) 常用方法:、 static BigDecimal valueOf(double) -> 此方法初始化小数时可以传入double类型数据 BigDecimal add(BigDecimal val) -> 加 返回其值为(this + val)的BigDecimal BigDecimal subtract(BigDecimalval) -> 减 返回值为(this - val)的BigDecimal BigDecimal multiply(BigDecimal val) -> 乘 返回其值为(this * val)的BigDecimal BigDecimal divide(BigDecimal val) -> 除 返回其值为(this / val)的BigDecimal // 加减乘除的方法和BigInteger一样的 // 这个除法如果除不尽是会报一个运算错误的 BigDecimal divide(BigDecimal divisor, int scale, int roundingMode) divisor:除数(除号后面的数) scale:指定保留几位小数 roundingMode:取舍方式 static int ROUND_UP -> 向上加一 static int ROUND_DOWN -> 直接舍去 static int ROUND_HALF_UP -> 四舍五入
double doubleValue() 将BigDecimal转换成doubleValue
之前的几个取舍方式是过时的,表示已经被新的内容的替代了
文档上加了 @Deprecated
就代表是过时的
1 2 3 4 5 6 7 8 新方法: BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) divisor:除数(除号后面的数) scale:指定保留几位小数 roundingMode(它的类型改变了):取舍方式 -> RoundingMode是一个枚举,里面的成员可以直接调用 UP -> 向上加一 DOWN -> 直接舍去 HALF_UP -> 四舍五入
这个时候黄线就没了
日期类 Date 1 2 3 4 5 6 7 8 9 10 表示特定时间,精确到毫秒 常识: 1、1000毫秒 = 1秒 2、时间原点1970年1月1日 0时0分0秒(UNIX系统起始时间),叫做格林威治时间,在0时区上 3、时区:北京位于东八区,一个时区经度差15度,时间相差一个小时,所以北京时间比时间原点时区时间相差八个小时 使用: 构造方法: Date() -> 获取系统时间 Date(long time) -> 获取指定时间,传递毫秒值 -> 从时间原点开始算
因为东八区,所以时间原点+了八个小时
1 2 3 4 5 Date date = new Date(); // 设置时间,从时间原点开始计算 date.setTime(1000L); // 获取设置的时间,返回毫秒值 System.out.println(date.getTime());
Calendar日历类 1 2 3 概述:日历类,抽象类(abstract) 获取方法:Calendar中的方法 static Calendar getInstance()
1 2 3 4 5 6 7 8 Calendar time = Calendar.getInstance();System.out.println(time); java.util.GregorianCalendar[time=1720776818898 ,areFieldsSet=true ,areAllFieldsSet=true ,lenient=true ,zone=sun.util.calendar.ZoneInfo[id="Asia/Shanghai" ,offset=28800000 ,dstSavings=0 ,useDaylight=false ,transitions=31 ,lastRule=null ],firstDayOfWeek=1 ,minimalDaysInFirstWeek=1 ,ERA=1 ,YEAR=2024 ,MONTH=6 ,WEEK_OF_YEAR=28 ,WEEK_OF_MONTH=2 ,DAY_OF_MONTH=12 ,DAY_OF_YEAR=194 ,DAY_OF_WEEK=6 ,DAY_OF_WEEK_IN_MONTH=2 ,AM_PM=1 ,HOUR=5 ,HOUR_OF_DAY=17 ,MINUTE=33 ,SECOND=38 ,MILLISECOND=898 ,ZONE_OFFSET=28800000 ,DST_OFFSET=0 ] 注意月份: 国外从0 开始,国内从1 开始,因此转换的时候月份要+1
java.util.GregorianCalendar[==time===1720776818898,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id=”Asia/Shanghai”,offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,==YEAR===2024,==MONTH===6,WEEK_OF_YEAR=28,WEEK_OF_MONTH=2,==DAY_OF_MONTH===12,DAY_OF_YEAR=194,==DAY_OF_WEEK===6,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,==HOUR===5,==HOUR_OF_DAY===17,==MINUTE===33,==SECOND===38,MILLISECOND=898,ZONE_OFFSET=28800000,DST_OFFSET=0]
字段值
含义
YEAR
年
MONTH
月
DAY_OF_DAY
日
HOUR AM_PM
时(12小时制)
HOUR_OF_DAY
时(24小时制)
MINUTE
分
SECOND
秒
DAY_OF_WEEK
周中的天(周日为1)
最前面返回的是实现类的对象,因为不同时区不一样,因此实现类也不一样
1 2 3 4 5 6 7 8 9 10 11 常用方法: int get(int field) -> 返回给指定日历字段的值 void set(int field, int value) -> 给定的日历字段设置为指定的值 void add(int field, int amount) -> 根据日历的规则,为给定的日历字段添加或者减去指定的时间量 Date getTime() -> 将Calendar转成Date对象 field:代表的是日历字段 -> 年、月、日、星期等,都是静态的 拓展方法: void set(int year, int month, int date) -> 直接设置年月日
比较无聊,也不演示了
1 2 3 4 5 概述:格式化日期,让获取到的日期看着舒服 构造: SipleDateFormat(String pattern) pathhern代表的是我们自己指定的日期格式-字母不能改变,中间的连接符可以改变 yyyy-MM-dd HH:mm:ss
时间字母表示
说明
y
年
M
月
d
日
H
时
m
分
s
秒
1 2 3 4 5 6 7 8 两个方法: String format(Date date) -> 将Date对象按照指定的格式转成String Date parse(String source) -> 将符合日期格式的字符串转化成Date对象 SipleDateFormat sdf = new SipleDateFormat("yyyy-MM-dd HH:mm:ss"); String time = sdf.format(new Date()); 注意:Date parse(String source)这东西有异常是个,编译时期异常,引用时需要处理,如果格式正确返回正确结果,格式错误就飘红
jdk8新日期类 LocalDate本地日期 1 2 3 4 概述:LocalDate是一个不可变的日期时间对象,表示日期,通常被视为年月日 获取: static LocalDate now() -> 创建LocalDate对象 static LocalDate of(int year, int month, int datOfMonth) -> 创建LocalDate对象,设置年月日
1 2 3 4 5 6 7 public static void main (String[] args) { LocalDate localDate = LocalDate.now(); System.out.println("localDate = " + localDate); LocalDate localDate1 = LocalDate.of(2000 ,10 ,10 ); System.out.println("localDate1 = " + localDate1); }
LocalDateTime对象 1 2 3 4 5 概述:LocalDateTime是一个不可变的日期时间对象,通常被视为年-月-日-时-分-秒 获取: static LocalDateTime now() -> 创建LocalDateTime对象 static LocalDateTime of(int year,Month month,int dayofMonth,int hour,int minute,int second) -> 创建Loca1DateTime对象,设置年月日时分秒
1 2 3 4 5 LocalDateTime localDateTime = LocalDateTime.now(); System.out.println("localDateTime = " + localDateTime); LocalDateTime localDateTime1 = LocalDateTime.of(2000,10,10,10,10,10); System.out.println("localDateTime1 = " + localDateTime1);
还可以往下设置毫秒
获取对应字段get开头 1 2 3 4 方法: int getYear() -> 获取年份 int getMonthValue() -> 获取月份 int getDayOfMonth() -> 获取月中的第几天
1 2 3 4 LocalDate localDate = LocalDate.now(); localDate.getYear() -> 获取年份 localDate.getMonthValue() -> 获取月份 localDate.getDayOfMonth() -> 获取月中的第几天
设置日期字段with开头 1 2 3 LocalDate withYear(int year) -> 设置年份 LocalDate withMonth(int month) -> 设置月份 LocalDate withDayOfMonth(int day) -> 设置月中的第几天(从0开始)
懒得写了,调用使用对象名.方法
日期字段偏移 1 2 设置日期字段的偏移量,方法名以plus开头,向后偏移 设置日期字段的偏移量,方法名以minus开头,向前偏移
这些方法顾名思义吧,都是点出来的
Period计算日期之间的偏差 1 2 3 4 5 6 7 8 方法: static Period between(LocalDate d1,LocalDate d2) -> 计算两个日期的差值 使用Period对象接收这个差值后,使用下面方法获取对应的差值 getYears() getMonths() getDays()
Duration计算时间之间的偏差 1 2 3 4 5 6 7 8 9 10 11 12 13 14 方法: static Duration between(Temporal startInclusive,Temporal endExclusive) -> 精确计算两个日期的差值 Temporal:一个接口 它实现类包含:LocalDate LocalDateTime 注意:这两个方法不同点在于参数,Duration需要传递Temporal的实现类对象,注意要传递LocalDateTime,因为Duration计算精确时间偏差,所以需要传递能操作精确时间的LocalDateTime 利用Duration获取相差的时分秒 -> to开头 toDays() toHours() toMinutes() toMillis() -> 获取相差毫秒
总结一下两个计算时间差值的类
Period就是硬减2024-10-12减2023-9-11获取相差的年月日都是1,很离谱
Duration2024-10-12减2023-9-11获取相差的时分秒是根据实际情况来的
1 2 3 4 5 6 7 8 9 获取: static DateTimeFormatter ofPattern(String pattern) -> 获取对象,指定格式 方法: String format(TemporalAccessor temporal) -> 将日期对象按照指定的规则转成String TemporalAccessor:接口,子接口包括Temporal Temporal的实现类LocalDate LocalDateTime TemporalAccessor parse(CharSeqence text) -> 将符合规则的字符串转成日期对象
1 2 3 4 5 6 DateTimeFormatter dtf =DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); Stringtime ="2000-10-1010:10:10"; TemporalAccessor temporalAccessor = dtf.parse(time); System.out.println(temporalAccessor); LocalDateTime localDateTime=LocalDateTime.from(temporalAccessor); System.out.println("localDateTime ="+localDateTime);
工具类 System 1 2 3 4 概述:系统相关类,是一个工具类 特点: 1、构造私有化,不能new对象 2、方法都是静态的,类名直接调用
方法
作用
static long currentTimeMillis()
返回以毫秒为单位的当前时间,可以测试效率使用
static void exit(int status)
终止当前正在运行的Java虚拟机
static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
数组复制 src:源数组 srcPos:从源数组的哪个索引开始复制 dest:目标数组 destPos:从目标数组哪个索引开始粘贴 length:复制多少个元素
1 2 System.out.println(System.currentTimeMillis()); // 输出的是当前时间
演示一下
==Arrays数组工具类== 1 2 3 4 5 概述:数组工具类 特点: 构造私有 方法静态(这是工具类的特点) 使用:类名直接调用
方法
说明
static String toString(int[] a)
按照格式打印数组元素 [元素1,元素2……]
static void sort(int[] a)
升序排序(底层原理并不是冒泡排序,效果相同)
static int binarySearch(int[] a, int key)
二分查找(前提是升序)
static int[] copyOf(int[] orginal, int newLength)
数组扩容
之前使用过的数组操作的方法都有封号好的类之间使用即可
排序的底层方法和之前手写的toString很相似
数组扩容的底层方法和之前手写也很相似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main (String[] args) { int [] arr = {1 ,4 ,6 ,2 ,8 ,4 ,1 ,9 ,5 ,3 ,2 }; System.out.println(Arrays.toString(arr)); Arrays.sort(arr); System.out.println(Arrays.toString(arr)); int [] arr1 = {1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 }; int index = Arrays.binarySearch(arr1,3 ); System.out.println(index); int [] arr2 = {1 ,2 ,3 ,4 ,5 }; arr2 = Arrays.copyOf(arr2,10 ); System.out.println(Arrays.toString(arr2)); }
包装类 1 2 3 4 5 6 概述:就是基本类型对应的类(包装类),我们需要将基本类型转成包装类,从而让基本类型拥有类的特性(基本类型转成包装类之后,就可以使用包装类中的方法操作数据) 为啥学包装类: 1、将来有一些特定场景,特定操作,比如调用方法传递包装类 比如:ArrayList集合,里面有一个方法add(Integer i),此时我们不能调用add方法之后直接传递基本类型,因为引用类型不能直接接收基本类型的值,就需要先将基本类型转成包装类,传递到add方法中 2、将来还可以将包装类转成基本类,因为包装类无法进行运算
基本类型
包装类
byte
Byte
short
Short
int
Integer
long
Long
float
Float
double
Double
char
Charactor
boolean
Boolean
Integer 1 2 3 4 5 6 概述:int的包装类 构造: 不推荐使用 Integer(int value) Integer(String s) s必须是数字 这样的构造方法是过时了的,输入就飘红了,注意这个构造方法是八个类型都有的,但是有一个特殊
1 2 3 4 5 6 Boolean bl = new Boolean(s:"true"); System.out.println("b1 = " + b1); Boolean b2 = new Boolean("false"); System.out.println("b2 ="+ b2); Boolean b3 = new Boolean("True"); System.out.println("b3 = " + b3);
b1、b2可以理解,但是b3这里是有说法的
输出还是 true
看一下底层
1 2 3 4 5 6 装箱: 将基本类型转成对应的包装类 方法: static Integer valueOf(int i) static Integer valueOf(String s)
1 2 3 4 5 6 7 public static void main (String[] args) { Integer i1 = Integer.valueOf(10 ); System.out.println("i1 = " + i1); Integer i2 = Integer.valueOf("100" ); System.out.println("i2 = " + i2); }
1 2 3 4 拆箱:将包装类转成基本类型 方法: int intValue() 其他类型的包装类的拆箱方法分别是类型名+Value()
1 2 3 4 5 6 7 public static void main (String[] args) { Integer i1 = Integer.valueOf(10 ); System.out.println("i1 = " + i1); int i = i1.intValue(); System.out.println("(i+10) = " + (i+10 )); }
自动拆箱装箱 1 2 3 4 5 6 7 8 拆箱和装箱多数时候都是自动完成,在idea内可以直接 Integer i = 10; 这个时候就发生了自动装箱 Integer sum = i+10; 自动拆箱又装箱
反编译可以看到
1 2 3 4 public static void main (String[] args) { Integer i = Integer.valueOf(10 ); Integer sum = Integer.valueOf(i.intValue() + 10 ); }
拓展一下:
很有意思的事情,同样Integer地址值不一样
看一下Integer中的装箱代码
往上一翻可以发现范围是[-128,127]
数组内是[-128,127]的Integer对象,如果传入的数在这个范围内,就会共享这个Integer对象
基本类型和String类之间的转换 基本类型转String 1 2 3 4 5 方式一: +"" 拼接 方式二:String内的静态方法 static String valueOf(int i)
String转成基本数据类型 1 每一个类中都有一个类似的方法: parseXXX
位置
方法
说明
Byte
static byte parseByte(String s)
将String转成byte类型
Short
static byte parseShort(String s)
将String转成short类型
Integer
static byte parseInteger(String s)
将String转成int类型
Long
static byte parseLong(String s)
将String转成long类型
Float
static byte parseFloat(String s)
将String转成float类型
Double
static byte parseDouble(String s)
将String转成double类型
Boolean
static byte parseBoolean(String s)
将String转成boolean类型
注意没有char类型
1 2 3 4 private staticvoid method02() { int number = Integer.parseInt("1111"); System.out.println(number+1); }
1 2 3 4 5 在实际开发过程中如何定义一个标准的javabean 之前说过一些,还要补充一个: 定义JavaBean的时候一般会将基本类型的属性定义成包装类 其实呢,就是在原有的基础上把基本数据类型改成包装类即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class javabeantest { private Integer uid; private String name; private String age; public javabeantest () { } public javabeantest (Integer uid, String age, String name) { this .uid = uid; this .age = age; this .name = name; } public String getName () { return name; } public void setName (String name) { this .name = name; } public String getAge () { return age; } public void setAge (String age) { this .age = age; } public Integer getUid () { return uid; } public void setUid (Integer uid) { this .uid = uid; } }
举例:如果uid为Integer型,默认值为null
1、将来JavaBean中的数据都是和数据库表联系起来的,我们可以将JavaBean中的数据添加到表中,如果表中的uid为主键自增,此时添加语句的uid中的数据不用我们单独进行赋值了
添加语句的sql语句就可以这样写:
insert into user(uid,name,age) value(NULL,”张三”,18)
2、到时候,我们需要将JavaBean中封装的数据获取出来放到sql语句中,如果uid为主键自增,而且JavaBean中的uid为包装类型,默认值为null,这样就不用单独维护uid的值了,也不用先给uid赋值再保存到数据库中了,就可以直接使用默认值,将默认值放到sql语句的uid列中
3、而且将JavaBean中的属性变为包装类,还可以使用包装类中的方法去操作此属性值
多线程 了解多线程 进程与线程 1 2 3 4 5 6 7 进程:在内存上执行的引用程序 线程:是进程中的最小执行单元 线程作用:负责当前进程中程序的运行,一个进程中至少有一个线程 在CPU和内存之间为每一个功能开辟对应的通道,方便CPU去内存中提取代码做计算,这个通道称之为“线程” 简单理解:一个功能就需要一条线程去执行
1、使用场景:软件耗时操作 -> 拷贝大文件,加载大量资源,聊天软件,后台服务器
一个线程可以干一件事,我们就可以同时做多件事
并发与并行 1 2 3 4 5 6 7 8 并行:在同一时刻,有多个执行在多个CPU上同时执行(就好比是多个人做不同的事) 比如:多个厨师在炒多个菜 并发:在同一时刻,有多个指令在单个CPU上(交替)执行 比如:一个厨师炒多个菜 1、之前CPU是单核,但是在执行多个程序的时候好像是在同时执行,原因是CPU在多个线程之间做高速切换 2、现在的CPU都是多核多线程了,比如2核4线程,那么CPU可以同时执行4个线程,但是如果多了,CPU就开始切换了,所以CPU在执行程序的时候并发和并行都存在
CPU调度 1 2 分时调度:指的是让所看的线程轮流获取CPU使用权,并且平均分配每个线程占用CPU的时间片 2.抢占式调度:多个线程轮流抢占CPU使用权,哪个线程先抢到了,哪个线程先执行,一般都是优先级高的先抢到CPU使用权的几率大,Java程序就是抢占式调度
主线程的概念:
CPU和内存之间为main方法开辟的通道专门为main方法服务,这个通道叫做主线程”
创建线程(重点) 继承Thread 1 2 3 4 1、定义一个类,继承Thread 2、重写run方法,在run方法中设置线程任务(所谓的线程任务指的是此线程要干的具体的事,具体执行的代码) 3、创建自定义线程类的对象 4、调用Thread的start方法,开启线程,jvm自动调用run方法
重写的run方法
1 2 3 4 5 6 7 8 public class mythread extends Thread { @Override public void run () { for (int i = 0 ; i <10 ; i++) { System.out.println("mythread....执行了" +i); } } }
main
1 2 3 4 5 6 7 8 9 10 11 public class test { public static void main (String[] args) { mythread p1 = new mythread (); p1.start(); for (int i = 0 ; i < 10 ; i++) { System.out.println("main....执行了" +i); } } }
可以清除的看到有抢占情况
多线程在内存中的运行:
开启一个线程会开启一个栈空间,去运行对应的线程代码,死循环开线程,电脑直接卡死
同一个线程对象只能调用一个start,不能连续调用start,想再开一个线程,就new一个新的线程对象
Thread中的方法 1 2 3 4 5 6 void start() -> 开启线程,jvm自动调用run方法 void run() -> 设置线程任务,这个run方法是Thread重写接口Runnable中的run方法 String getName() -> 获取线程名字 默认是Thread-i i从0递增 String setName() -> 设置线程名字 static Thread currentThread() -> 获取正在执行的线程对象 static void sleep(long millis) -> 线程休眠,单位是毫秒
复习一下异常的知识,这里继承的Thread是不能抛异常的,原因是Thread中的run方法没有抛异常,这里也不能上抛,只能try……catch
如果我想知道主线程的线程名称,我的main方法又没有继承Thread很显然不能getName,这个时候就需要currentThread()方法,获得当先运行的Thread对象,写一个链式调用即可
Thread.currentThread().getName()
返回结果是main
Thread中的其他方法 1 2 3 4 5 6 7 8 9 void setPriority(int newPriority) -> 设置线程优先级,优先级越高的线程,抢到CPU使用权的几率越大,但是不是每次都能抢到 int getPriority() -> 获取线程优先级 void setDaemon(boolean on) -> 设置为守护线程 执行完非守护线程,守护线程就要结束,无论是否执行完毕 static void yie1d() -> 礼让线程,让当前线程让出CPU使用权 void join() -> 插入线程或者插队线程
优先级 可以看到优先级都为5
看一下底层代码,最小为1,默认为5,最大为10
但是效果并不是特点明显,最高优先级也不是每次都能抢到
守护线程 main
1 2 3 4 5 6 7 8 9 10 11 12 13 public class test { public static void main (String[] args) { mythread p1 = new mythread (); mythread01 p2 = new mythread01 (); p1.setName("张三" ); p2.setDaemon(true ); p1.start(); p2.start(); } }
守护线程
1 2 3 4 5 6 7 8 public class mythread01 extends Thread { @Override public void run () { for (int i = 0 ; i <100 ; i++) { System.out.println("李四....执行了" +i); } } }
非守护线程
1 2 3 4 5 6 7 8 public class mythread extends Thread { @Override public void run () { for (int i = 0 ; i <10 ; i++) { System.out.println("mythread....执行了" +i); } } }
可以看到其他线程结束后,守护线程也结束了,并没有循环完100次
守护线程也不是马上结束了,当非守护线程结束之后,系统会告诉守护线程,告知的过程中,守护线程仍在执行,我感觉使用场景很适合游戏反作弊
礼让线程 写在一个实现类内,理想型是p1、p2交叉执行,但是并不是绝对的,只是尽可能的平衡,即使礼让了仍然会有连续执行的情况
插入线程
main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class test { public static void main (String[] args) throws InterruptedException { mythread p1 = new mythread (); p1.setName("张三" ); p1.start(); p1.join(); for (int i = 0 ; i < 10 ; i++) { System.out.println("main执行了" ); } } }
实现Runnable接口 1 2 3 4 5 6 Thread实现了Runnable接口,重写了run方法,Runnable接口中只要一个run方法 1、创建类,实现Runnable接口 2、重写run方法,设置线程任务 3、利用Thread类的构造方法:Thread(Runnable target),创建Thread对象(线程对象),将自定义的类当参数传递到Thread构造中 -> 这一步是让我们自己定义的类成为一个真正的线程类对象 4、调用Thread中的start方法,开启线程,jvm自动调用run方法
实现类
1 2 3 4 5 6 7 8 public class MyRunnable implements Runnable { @Override public void run () { for (int i = 0 ; i < 10 ; i++) { System.out.println(Thread.currentThread().getName()+"...执行了" +i); } } }
main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class test { public static void main (String[] args) { MyRunnable m1 = new MyRunnable (); Thread t1 = new Thread (m1); t1.start(); for (int i = 0 ; i < 10 ; i++) { System.out.println(Thread.currentThread().getName()+"执行" +i); } } }
两种创建方法的区别
1、继承Thread:继承只支持单继承,有继承的局限性
2、实现Runnable:没有继承的局限性
MyRunnable extends Fu implements Runnable
总结就是:Runnable节省了一个继承卡槽
匿名内部类创建多线程
严格意义来说,匿名内部类方式不属于创建多线程方式其中之一,因为匿名内部类形式建立在实现Runnable接口或者继承Thread的基础上完成的
1 2 3 4 5 6 7 8 9 10 11 回顾: new 接口/抽象类(){ 重写方法 }.重写的方法() or 接口名/类名 对象名 = new 接口/抽象类(){ 重写方法 } 对象名.重写的方法();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Test01 { public static void main (String[] args) { new Thread ("张三" ){ @Override public void run () { for (int i = 0 ; i < 10 ; i++) { System.out.println(Thread.currentThread().getName()+i); } } }.start(); new Thread (new Runnable () { @Override public void run () { for (int i = 0 ; i < 10 ; i++) { System.out.println(Thread.currentThread().getName()+i); } } },"李四" ).start(); } }
Thread也为匿名内部类提供了命名的方法:
继承Thread的时候,直接传入一个字符串,会给线程命名
实现Runnable的时候,传入一个Runnable对象的同时再传入一个字符串实现命名
线程安全
什么时候发生:
多个线程访问同一个资源时,导致了数据出现问题
可以看到三个人访问了同一个,甚至还买到了第0张
线程有问题的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class myRunnable implements Runnable { int ticker = 100 ; @Override public void run () { while (true ){ if (ticker >= 0 ) { System.out.println(Thread.currentThread().getName()+"买了第" +ticker+"张票" ); ticker--; } else { break ; } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test { public static void main (String[] args) { myRunnable p1 = new myRunnable (); Thread t1 = new Thread (p1,"张三" ); Thread t2 = new Thread (p1,"李四" ); Thread t3 = new Thread (p1,"王五" ); t1.start(); t2.start(); t3.start(); } }
原因:CPU在不同线程之间做高速切换导致的,在前一个人进入线程还没还得及ticket–的时候,下一个人也访问了进来,就造成了有3个第100的现象,0也是这样,前一个人还没–,就通过if判断进入,前者–,后者输出ticket就出现了0
同步代码块 1 2 3 4 5 6 7 8 9 10 问题抛出来了,肯定得解决 格式: synchronized(任意对象){ 线程可能出现的不安全代码 } 1、任意对象:就是锁对象 2、执行: 一个线程拿到锁之后,会进入到同步代码块中执行,在此期间,其他线程拿不到锁,就进不去同步代码块,需要在同步代码块外面等待排队,需要等着执行的线程执行完毕,出了同步代码块,相当于释放锁,等待的线程才能抢到锁,才能进入到同步代码块中执行 3、默认锁:this
解决问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class myRunnable implements Runnable { int ticker = 100 ; Object obj = new Object (); @Override public void run () { while (true ){ try { Thread.sleep(100L ); } catch (InterruptedException e) { throw new RuntimeException (e); } synchronized (obj){ if (ticker > 0 ) { System.out.println(Thread.currentThread().getName()+"买了第" +ticker+"张票" ); ticker--; } else { break ; } } } } }
这样的方式既可以解决访问用一个对象,还加了一个sleep防止全被同一个人拿走
锁只能有一把,不能多把,联合实际也可以理解
同步方法 普通同步方法 1 2 3 4 5 格式: 修饰符 synchronized 返回值类型 方法名(参数){ 方法体 return 结果 }
实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class myRunnable implements Runnable { int ticker = 100 ; @Override public void run () { while (true ){ try { Thread.sleep(100L ); } catch (InterruptedException e) { throw new RuntimeException (e); } testMethod(); } } public synchronized void method () { if (ticker > 0 ) { System.out.println(Thread.currentThread().getName()+"买了第" +ticker+"张票" ); ticker--; } } public void testMethod () { synchronized (this ){ if (ticker > 0 ) { System.out.println(Thread.currentThread().getName()+"买了第" +ticker+"张票" ); ticker--; } } } }
上方代码块内method是普通同步方法,虽然没有指定锁,但是这个锁是this,也就是说,普通同步方法method和下方的同步代码块testMethod是一样的
静态同步方法 1 2 3 4 5 6 7 格式: 修饰符 static synchronized 返回值类型 方法名(参数){ 方法体 return 结果 } 默认锁:class对象
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class myRunnable implements Runnable { static int ticker = 100 ; @Override public void run () { while (true ){ try { Thread.sleep(100L ); } catch (InterruptedException e) { throw new RuntimeException (e); } testMethod01(); } } public static synchronized void method01 () { if (ticker > 0 ) { System.out.println(Thread.currentThread().getName()+"买了第" +ticker+"张票" ); ticker--; } } public static void testMethod01 () { synchronized (myRunnable.class){ if (ticker > 0 ) { System.out.println(Thread.currentThread().getName()+"买了第" +ticker+"张票" ); ticker--; } } } }
因为静态成员不能访问非静态成员,所以需要将ticket变为静态的,然后锁变为了class,其他的和普通同步方法很相似
拓展一下:之前说过StringBuilder多线程时不安全,但是效率高,StringBuffer安全但是效率低
原因就是StringBuffer的方法都是带synchronized的
死锁 死锁是指两个或者两个以上的线程在执行过程中由于竞争同步锁而产生的一种阻塞现象;如果没有外力的作用,他们将无法继续执行下去,这种情况称之为死锁
如图所示:线程1正在持有锁1,但是线程1必须再拿到锁2,才能继续执行
而线程2正在持有锁2,但是线程2需要再拿到锁1,才能继续执行
此时两个线程处于互相等待的状态,就是死锁,在程序中的死锁将出现在同步代码块的嵌套中
因此,我们应该==避免同步代码的嵌套==
实现一个死锁看看
1 2 3 public class LockA { public static LockA lockA = new LockA (); }
1 2 3 public class LockB { public static LockB lockB = new LockB (); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Run implements Runnable { private boolean flag; public Run (boolean flag) { this .flag = flag; } public Run () { } @Override public void run () { if (flag){ synchronized (LockA.lockA){ System.out.println("if...LockA" ); synchronized (LockB.lockB){ System.out.println("if...LockB" ); } } } else { synchronized (LockB.lockB){ System.out.println("else...LockA" ); synchronized (LockA.lockA){ System.out.println("else...LockB" ); } } } } }
1 2 3 4 5 6 7 8 9 public class Test { public static void main (String[] args) { Run p1 = new Run (true ); Run p2 = new Run (false ); new Thread (p1).start(); new Thread (p2).start(); } }
这种情况大概率死锁,但也有小概率手快,两个都拿了,执行完毕释放出来。
看个乐呵,还是要避免嵌套
线程生命周期 1 2 线程被创建并启动后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程中生命周期中,有几种状态呢?在API中Java.lang.Thread.State这个枚举中给出了六种线程状态: 这里先列出各个线程状态发生的条件,下面将会每种状态进行详细解析。
线程状态
导致状态发生体哦阿健
NEW(新建)
线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行)
线程可以在Java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器
Blocked(锁阻塞)
当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁,该线程将变成Runnable状态
Waiting(无线等待)
一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒
Timed Waiting(计时等待)
同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这个状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep、Object.wait。
Terminated(被终止)
因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。或者调用过时方法stop()
画图表示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 注意以下方法 1、sleep(time)和wait(time)的区别 sleep(time):线程睡眠,在睡眠的过程中,线程不会释放锁,此时其他线程抢不到锁,设置时间一旦超时,自动醒来,继续执行 wait(time):线程等待,在等待的过程中会释放锁,其他线程就可能抢到锁,如果在等待的过程中被唤醒或者时间超时,会和其他的线程重新抢锁,如果抢到了继续执行,抢不到进入锁阻塞 2、wait()和notify() wait():空参wait,线程进入无限等待状态,会释放锁,需要其他线程调用notify(一次唤醒一条等待的线程,唤醒的线程是随机的)或者notifyAll方法(将所有等待线程全唤醒),被唤醒之后,会和其他的线程重新抢锁,如果抢到了继续执行,抢不到进入锁阻塞 notify():notify会唤醒正在等待的线程,一次只能唤醒一条等待的线程;如果多线程等待,随机唤醒一条 notifyAll():唤醒所有等待的线程 3、wait和notify两个方法的用法: 两个方法都需要锁对象调用,所以两个方法需要用到同步代码块、同步方法中 俩个方法的调用必须是同一个锁对象调用,可以理解为用同一个锁对象,将多条线程分到了一组中,这样notify就知道唤醒的是自己本组的等待线程
等待唤醒机制 1 需求:一个线程生产,一个线程消费,不能连续生产,不能连续消费 -> 等待唤醒机制(线程之间通信)
方法
说明
void wait()
线程等待,等待的过程会释放锁,需要被其他线程调用notify方法将其唤醒,重新抢锁执行
void notify()
线程唤醒,一次唤醒一个等待线程,多条线程等待,随机唤醒一条线程
void notifyAll()
线程唤醒,唤醒所有等待线程
wait和notify方法需要锁对象调用,所以需要用到同步代码块中,而且必须是同一锁对象
案例代码
思路:
1、怎么生产和消费包子
count++,count–
2、怎么证明有没有包子
设置一个flag flag=true表示有
3、如何防止生产到一半,CPU切换
加锁
4、如何保证生产一个消费一个,防止连续生产,连续消费
wait和notify方法
生产流水线
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.sjjws.h_stop;public class suo implements Runnable { private Test test; public suo (Test test) { this .test = test; } @Override public void run () { while (true ){ try { Thread.sleep(100L ); } catch (InterruptedException e) { throw new RuntimeException (e); } synchronized (test){ if (test.isFlag()) { try { test.wait(); } catch (InterruptedException e) { throw new RuntimeException (e); } } else { test.setCount(); test.setFlag(true ); test.notify(); } } } } }
消费流水线
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.sjjws.h_stop;public class baoz implements Runnable { private Test test; public baoz (Test test) { this .test = test; } @Override public void run () { while (true ){ try { Thread.sleep(100L ); } catch (InterruptedException e) { throw new RuntimeException (e); } synchronized (test){ if (test.isFlag()==false ) { try { test.wait(); } catch (InterruptedException e) { throw new RuntimeException (e); } } else { test.getCount(); test.setFlag(false ); test.notify(); } } } } }
包子铺
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.sjjws.h_stop;public class Test { int count; boolean flag = false ; public boolean isFlag () { return flag; } public void setFlag (boolean flag) { this .flag = flag; } public void getCount () { System.out.println("消费第" +count+"个包子" ); } public void setCount () { count++; System.out.println("生产第" +count+"个包子" ); } }
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.sjjws.h_stop;public class Test01 { public static void main (String[] args) { Test test = new Test (); suo p1 = new suo (test); baoz p2 = new baoz (test); Thread t1 = new Thread (p1); Thread t2 = new Thread (p2); t1.start(); t2.start(); } }
还可以使用同步方法来完成
生产
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.sjjws.h_stop;public class suo implements Runnable { private Test test; public suo (Test test) { this .test = test; } @Override public void run () { while (true ){ try { Thread.sleep(100L ); } catch (InterruptedException e) { throw new RuntimeException (e); } test.setCount(); } } }
消费
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.sjjws.h_stop;public class baoz implements Runnable { private Test test; public baoz (Test test) { this .test = test; } @Override public void run () { while (true ){ try { Thread.sleep(100L ); } catch (InterruptedException e) { throw new RuntimeException (e); } test.getCount(); } } }
包子铺
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.sjjws.h_stop;public class Test { int count; boolean flag = false ; public boolean isFlag () { return flag; } public void setFlag (boolean flag) { this .flag = flag; } public synchronized void getCount () { if (this .flag==false ) { try { this .wait(); } catch (InterruptedException e) { throw new RuntimeException (e); } } else { this .flag = false ; System.out.println("消费第" +count+"个包子" ); this .notify(); } } public synchronized void setCount () { if (this .flag) { try { this .wait(); } catch (InterruptedException e) { throw new RuntimeException (e); } } else { count++; System.out.println("生产第" +count+"个包子" ); this .flag = true ; this .notify(); } } }
多等待多唤醒案例
如果有多条线程的话,上面的方法还是有点问题的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Test01 { public static void main (String[] args) { Test test = new Test (); suo p1 = new suo (test); baoz p2 = new baoz (test); new Thread (p1).start(); new Thread (p1).start(); new Thread (p1).start(); new Thread (p2).start(); new Thread (p2).start(); new Thread (p2).start(); } }
症结在于notify,因为是随机唤醒,这个机制面对6个对象抢锁的时候会出现问题,因此面对多线程的时候要使用notifyAll来防止没有线程来抢锁了
因此只需要改变notify即可
不改变:这个情况是全都wait了,卡死了,全休眠去了,没有活着的线程了
这里老师讲的很迷,连续消费,连续生产的问题,只需要加上应该else就可以解决,因为他的代码没有else出了if就还会继续执行下面的语句
他之所以这样写,是为了铺垫下面的把if改成while,就是循环判断,防止唤醒之后,没有判断就继续执行代码,仔细想一下还是while比较安全、迅速一些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public synchronized void getCount () { while (!this .flag) { try { this .wait(); } catch (InterruptedException e) { throw new RuntimeException (e); } } this .flag = false ; System.out.println("消费。。。。。。。第" +count+"个包子" ); this .notifyAll(); } public synchronized void setCount () { while (this .flag) { try { this .wait(); } catch (InterruptedException e) { throw new RuntimeException (e); } } count++; System.out.println("生产第" +count+"个包子" ); this .flag = true ; this .notifyAll(); }
Lock锁 1 2 3 4 5 6 7 8 9 概述: Lock是一个接口 实现类: ReentrantLock 方法: lock() -> 获取锁 unlock() -> 释放锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package com.sjjws.j_lock;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class myRunnable implements Runnable { int ticker = 100 ; Lock lock = new ReentrantLock (); @Override public void run () { while (true ) { try { Thread.sleep(100L ); } catch (InterruptedException e) { throw new RuntimeException (e); } lock.lock(); if (ticker > 0 ) { System.out.println(Thread.currentThread().getName() + "买了第" + ticker + "张票" ); ticker--; } else { break ; } lock.unlock(); } } } import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class myRunnable implements Runnable { int ticker = 100 ; Lock lock = new ReentrantLock (); @Override public void run () { while (true ) { try { Thread.sleep(100L ); lock.lock(); if (ticker > 0 ) { System.out.println(Thread.currentThread().getName() + "买了第" + ticker + "张票" ); ticker--; } else { break ; } } catch (InterruptedException e) { throw new RuntimeException (e); }finally { lock.unlock(); } } } }
synchronized:不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放锁对象
Lock:是通过两个方法控制需要被同步的代码,形式上更加灵活
实现多线程方式3-Callable接口 1 2 1、继承Thread 2、实现Runnable接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Callable<V>是一个接口,类似于Runnable 方法: V call() -> 设置线程任务,类似于run方法 <V>: 泛型 用于指定我们操作什么类型的数据,<>内只能写引用数据类型,也就是说基本数据类型要转换成包装类,如果泛型不写,默认是Object类型数据 实现callable接口时,指定泛型是什么类型的,重写call方法返回值就是什么类型 获取call方法返回值:FutureTask<V> 1、FutureTask<V>实现了一个接口:Future<V> 2、FutureTask<V>中有一个方法: // FutureTask是Future的实现类 V get() -> 获取call方法的返回值
run方法和call方法的区别:
相同点:
设置线程任务
不同点:
call方法有返回值,有异常可以throws
run方法没有返回值,有异常不能throws
实现
1 2 3 4 5 6 7 8 import java.util.concurrent.Callable;public class mycall implements Callable <String> { @Override public String call () throws Exception { return "隔壁老王和金莲不得不说的故事" ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;public class Test { public static void main (String[] args) throws ExecutionException, InterruptedException { mycall call = new mycall (); FutureTask<String> futureTask = new FutureTask <>(call); Thread t1 = new Thread (futureTask); t1.start(); System.out.println(futureTask.get()); } }
实现多线程方式4-线程池 1 2 之前来一个线程任务,就需要创建一个线程对象去执行,用完还需要销毁线程对象,如果线程任务多了,就需要频繁创建线程对象和线程对象,这样会耗费内存资源,所以我们就想到线程对象能不能循环利用,用的时候直接拿线程对象,用完还回去
线程池:
创建线程池对象,指定最多能有几个线程对象
未到达上限时,如果没有线程对象,创建线程对象,用还还回去
到达上限后,等其他线程任务执行完毕归还线程对象后,再使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 创建线程池对象: 工具类:Executors 获取线程池对象: Executors中的静态方法 static ExecutorService newFixedThreadPool(int nThreads) 1、参数:指定线程池中最多创建的线程对象条数 2、返回值ExecutorService是线程池,用来管理线程对象 执行线程任务:ExceutorService中的方法 Future<?> submit(Runnable task) -> 提交一个Runnable任务,用于执行 Future<T> submit(Callable<T> task) -> 提交一个Callable任务用于执行 submit方法的返回值:Future接口 用于接收run方法或者call方法返回值的,但是run方法没有返回值,所以不用Future接收,执行call方法需要用Future接收 Future中有一个方法:V get() -> 获取call方法的返回值 ExecutorService中的方法: void shutdown() -> 启动有序关闭,其中先前提交的任务被执行,但不会接收新的任务(关闭线程池)
定义线程任务
1 2 3 4 5 6 7 8 9 10 11 12 package com.sjjws.k_call;import java.util.concurrent.Callable;public class mycall implements Callable <Integer> { @Override public Integer call () throws Exception { System.out.println(Thread.currentThread().getName()+"执行了" ); return 1 ; } }
执行方法一——run方法使用较多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class method { public static void main (String[] args) { ExecutorService es = Executors.newFixedThreadPool(2 ); es.submit(new mycall ()); es.submit(new mycall ()); es.submit(new mycall ()); es.shutdown(); } }
执行方法二——获取返回值(call)
1 2 3 4 5 6 7 8 9 import java.util.concurrent.*; public class method01 { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService es = Executors.newFixedThreadPool(2); Future<Integer> future = es.submit(new mycall()); System.out.println(future.get()); } }
定时器 Timer 1 2 3 4 5 6 7 8 9 不属于上面的内容,算作一个补充 构造: Timer() 方法: void schedule(TimerTask task, Date firstTime, long period) task:抽象类,是Runnable的实现类 firstTime:从什么时间开始执行,一般写new Date period:每隔多长时间执行一次,设置的是毫秒值
集合 集合框架(单列集合) 1 2 3 4 5 6 7 8 9 10 11 12 13 之前学了的保存数据的有:变量、数组、但是数组有一个很大的缺点——定长,如果增删改数据,数组并不好用,需要创建新数组 集合是一个长度可变的容器 特点: 1、只能存储引用数据类型数据 2、长度可变 3、集合中有大量的方法,方便操作 分类: 1、单列集合:一个元素就一个组成部分 list.add("张三") 2、双列集合:一个元素有两个组成部分(key value)键值对一一对应(字典) map.put("李四","王五")
框架介绍: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 单列集合顶级接口: Collection接口 Collection下有两个接口list和set list有三个实现类ArrayList、LinkedList、Vector ArrayList: 1、元素有序 2、元素可重复 3、有索引 4、线程不安全 5、底层数据结构是数组 LinkedLink: 1、元素有序 2、元素可重复 3、有索引 4、线程不安全 5、底层数据结构是双向链表 Vector: 1、元素有序 2、元素可重复 3、有索引 4、线程不安全 5、底层数据结构是数组 // 元老级别的了,因为安全所以慢,用的也少 set接口也有三个实现类HashSet、LinkedHashSet、TreeSet HashSet: 1、元素无序 2、元素唯一 3、无索引 4、线程不安全 5、底层数据结构是哈希表 LinkedHashSet: 还是HashSet的子类 1、元素有序 2、元素唯一 3、无索引 4、线程不安全 5、底层数据结构是哈希表+双向链表 LinkedList本质上无索引,但是Java为其提供了很多根据索引操作元素的方法 TreeSet: 1、可对元素进行排序 2、元素唯一 3、无索引 4、线程不安全 5、底层数据结构是红黑树
Collection接口 1 2 3 4 5 6 7 8 9 概述:单列集合的顶级接口 使用: 创建: Collection<E> 对象名 = new 实现类对象<E>() <E>泛型: 决定了集合中能存储什么类型的数据,可以统一元素类型 泛型中只能写引用数据类型,因此集合只能存储引用数据类型,如果不写,默认是Object类型,此时什么类型的数据都可以存储 细节: 我们等号前面的泛型必须写,等号后面的泛型可以不写,jvm可以根据前面的泛型推导出后面的泛型是啥
1 2 3 4 5 6 7 8 9 常用方法: boolean add(E e):将给定的元素添加到当前集合中(我们一般调add时,不用boolean接收,因为add一定会成功) boolean addAll(collection<?extendsE>c):将另一个集合元素添加到当前集合中(集合合并)(把括号内的元素添加到后面) void clear():清除集合中所有的元素 boolean contains(object o):判断当前集合中是否包含指定的元素 boolean isEmpty():判断当前集合中是否有元素->判断集合是否为空 boolean remove(object o):将指定的元素从集合中删除 int size():返回集合中的元素个数。 object[] toArray():把集合中的元素,存储到数组中
为啥说add一定成功呢,因为如果传入的类型不正确,会出现编译时期异常
实践一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import java.util.ArrayList;import java.util.Arrays;import java.util.Collection;public class Demo01collection { public static void main (String[] args) { Collection<String> c1 = new ArrayList <>(); c1.add("张三" ); c1.add("李四" ); c1.add("王五" ); c1.add("张三" ); c1.add("张三" ); c1.add("张三" ); System.out.println(c1); Collection<String> c2 = new ArrayList <>(); c2.add("王二麻子" ); c2.add("王二麻子" ); c2.add("王二麻子" ); c2.add("王二麻子" ); c2.add("王二麻子" ); System.out.println("c2 = " + c2); c1.addAll(c2); System.out.println("c1 = " + c1); c2.clear(); System.out.println("c2 = " + c2); System.out.println(c1.contains("赵四" )); System.out.println("c1中无元素" +c1.isEmpty()); System.out.println("c2中无元素" +c2.isEmpty()); System.out.println("c1 = " + c1); c1.remove("王五" ); System.out.println("c1 = " + c1); System.out.println(c1.size()); Object[] arr = c1.toArray(); System.out.println(Arrays.toString(arr)); } }
迭代器 1 2 3 4 5 6 7 8 9 概述:主要作用就是遍历集合 需要用到一个接口:Iterator 获取: Inerator<E> iterator() // 这是collection中的一个方法 方法: boolean hasNext() -> 判断集合中有没有下一个元素 E next() -> 获取下一个元素
注意:next方法在获取的时候不要连续使用多次
很好理解,告诉你下一个有,结果一次拿两个,第二个可能拿不到,报错==NoSuchElementException==:没有操作的元素异常
说说我自己的看法,这个迭代器的作用就是用来遍历集合,通过for循环或者增强的for循环也可以解决
迭代器的迭代过程
这里定义了一个负一,这样查看下一个是否存在的方法就可以检验索引了,这样迭代的过程就很好理解了,和for循环
1 2 int cursor; int lasRet = -1;
迭代器底层原理 1 2 3 4 获取Iterator的时候怎么获取 Iterator iterator = list.iterator() 我们知道Iterator是一个接口。等号右边一定是它的实现类对象 Iterator接收的是哪个实现类对象呢? -> ArrayList中的内部类Itr对象
注意:
只要ArrayList使用迭代器的时候Iterator接口才会是Itr来实现,其他的集合使用迭代器,不是由Itr来实现的
例如:HashSet做迭代指向KeyIterator
并发修改异常 1 需求:定义一个集合,存储 唐僧、悟空、猪八戒、沙和尚,要求遍历到猪八戒的时候在后面添加上白龙马
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Demo02collection { public static void main (String[] args) { ArrayList<String> list = new ArrayList <>(); list.add("唐僧" ); list.add("悟空" ); list.add("猪八戒" ); list.add("沙和尚" ); Iterator iterator = list.iterator(); while (iterator.hasNext()) { String em = iterator.next().toString(); if ("猪八戒" .equals(em)) { list.add("白龙马" ); } } } }
这个时候异常就来了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 private class Itr implements Iterator <E> { int cursor; int lastRet = -1 ; int expectedModCount = modCount; ………………………… public E next () { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException (); Object[] elementData = ArrayList.this .elementData; if (i >= elementData.length) throw new ConcurrentModificationException (); cursor = i + 1 ; return (E) elementData[lastRet = i]; } final void checkForComodification () { if (modCount != expectedModCount) throw new ConcurrentModificationException (); }
结论:
当预计操作次数和实际操作次数不相等时,抛出并发修改异常
异常原因:
1 2 3 4 5 public boolean add(E e) { modCount++; add(e, elementData, size); return true; }
add方法上来就是一个modCount++,让实际操作次数+1,再次调用next方法的时候并没有重新把修改后的modCount赋值给expectedModCount,导致next方法底层判断实际操作次数和预期操作次数不相等
也不是没有方法搞定 ListIterator
这个和ArrayList有关
1 2 3 4 5 6 7 8 ListIterator<String> listIterator = list.listIterator(); while(listIterator.hasNext()){ String element = listIterator.next(); if("猪八戒".equals)element)){ listIterator.add("白龙马"); } } System.out.println(list);
因为ListIterator是有add这个方法的,所以可以正常添加
但是呢:使用迭代器、增强for,迭代集合的过程中,不要随意修改集合长度
数据结构 1 数据结构是一种具有一定逻辑关系,在计算机中应用某种存储结构,并且封装了相应操作的数据元素集合,它包含三方面的内容,逻辑关系、存储关系以及操作。
为什么需要数据结构
1 2 3 随着应用程序变得越来越复杂和数据越来越丰富,几百万、几十亿甚至几百亿的数据就会出现,而面对这么大对数据进行搜素插入或者排序等的操作就越来越慢。数据结构就是来解决这个问题的 数据结构非常复杂,这里只对数据结构进行简单的了解
栈
队列
数组 1 2 3 4 5 6 7 8 9 特点: 查询快,增删慢 查询快: 因为有索引,我们可以直接通过索引操作元素 增删慢: 数组定长 添加元素:创建新数组,将老数组中的元素复制到新数组中去,在最后添加元素;但是如果在中间添加元素就麻烦了,插入完新元素,后面的元素都要向后移动 删除元素:创建新数组,将老数组中的元素复制到新数组中去,要删除的元素不赋值;如果在中间删除元素,被删除元素后面的元素都要往前移动
链表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 在集合中涉及了两种链表:单向链表、双向链表 单向链表: 1、节点:一个节点分为两个部分 第一部分:数据域(存储数据) 第二部分:指针域(保存下一个节点地址值) 2、特点:前面节点保存后面节点的地址,但是后面节点地址不记录前面节点地址 双向链表: 1、节点:一个节点分为三部分 第一部分:指针域(保存上一个节点地址值) 第二部分:数据域(保存的数据) 第三部分:指针域(保存下一个节点地址值) 2、特点:前面节点记录后面节点地址,后面节点也记录前面节点地址 链表结构特点:查询慢,增删快
单向链表
双向链表
List接口 list接口是collection接口的子接口
常见的实现类:ArrayList、LinkedList、Vector
ArrayList集合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ArrayList是List接口的实现类 特点: 1、元素有序 2、元素可重复 3、有索引 4、线程不安全 数据结构:数组 常用方法: boolean add(E e) -> 将元素添加到集合中(尾部)(之前说过,这个一旦执行,一定成功,所以不需要接收返回值) void add(int index, E element) -> 在指定位置添加元素 boolean remove(Object o) -> 删除指定元素,删除成功为true E remove(int index) -> 删除指定索引位置上的元素,返回值是被删除的哪个元素 E set(int index, E element) -> 将指定索引位置上的元素,修改为后面的element元素 E get(int index) -> 根据索引获取元素 int size() -> 获取集合元素个数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class Demo01 { public static void main (String[] args) { ArrayList<String> list = new ArrayList <>(); list.add("张三" ); list.add("赵四" ); list.add("李四" ); list.add("王五" ); System.out.println("list = " + list); list.add(0 ,"二柱" ); System.out.println("list = " + list); list.remove("赵四" ); System.out.println("list = " + list); list.remove(3 ); System.out.println("list = " + list); list.set(1 ,"法外狂徒张三" ); System.out.println("list = " + list); list.get(0 ); System.out.println("list = " + list); System.out.println(list.size()); } }
遍历集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Demo01for { public static void main (String[] args) { ArrayList<String> list = new ArrayList <>(); list.add("二柱" ); list.add("张三" ); list.add("赵四" ); list.add("李四" ); list.add("王五" ); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } System.out.println("------我是分割线------" ); for (int i = 0 ; i < list.size(); i++) { System.out.println(list.get(i)); } } }
remove问题
解决方法
1 2 3 4 5 list.remove(Integer.valueOf(2 )); list.remove(new Integer (2 ));
底层源码 1 2 3 4 5 6 7 8 9 构造方法: ArrayList() -> 构造一个初始容量为10的空列表 ArrayList(int initialCapacity) -> 构造具有指定初始容量的空列表 ArrayList问题总结: 容量为10并不是new出来之后容量就是10,而是第一次add方法执行之后,扩容到10 ArrayList底层为数组,为什么说容量可变呢,因为底层自动扩容 -> Arrays.copyOf() 扩容1.5倍(原长度+原长度二进制数据右移一位)
无参构造
可以看到,并没有所谓的容量为10的空列表,长度为0
注意:
说的容量为10是==第一次执行add方法后==
那就看一下add方法吧
超出容量之后,再次执行add时
SOFT_MAX_ARRAY_LENGTH是数组最大长度2147483639
返回新数组后执行
1 return elementData = Arrays.copyOf(elementData, newCapacity);
将原数组的数据赋值给新数组
有参构造
ArrayList list = new Array<>(); -> 现在都是想用就new
但是将来开发不会想用就new集合,而是调用一个方法,查询出很多数据来,此方法返回一个集合,自动将查询出来的数据放到集合中,我们再在页面上展示数据,遍历集合
而且将来调用方法,返回的集合类型,一般都是接口类型
List list = 对象.查询方法()
LinkedList集合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 概述:是List接口的实现类 特点: 1、元素有序 2、元素可重复 3、有索引 -> 这里说有索引指的是有操作索引的方法,不代表本质上有索引 4、线程不安全 数据结构:双向链表,索引是数组的东西 方法:有大量操作首尾元素的方法 public void addFirst(E e) -> 将指定元素插入到列表的开头 public void addLast(E e) -> 将指定元素添加到列表的结尾 public E getFirst() -> 返回列表的第一个元素 public E getLast() -> 返回列表的最后一个元素 public E removeFirst() -> 删除列表的第一个元素 public E removeLast() -> 删除列表最后一个元素 public E pop() -> 从此列表表示的堆栈处弹出一个元素 public void push(E e) -> 将元素推入此列表所表示的堆栈 public boolean isEmpty() -> 如果列表没有元素,则返回true
虽然没有索引但是也可以使用get方法配合for循环遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Demo03 { public static void main (String[] args) { LinkedList<String> linkedList = new LinkedList <>(); linkedList.add("大头儿子" ); linkedList.add("大头cs" ); linkedList.add("小头爸爸" ); linkedList.add("小头孙子" ); Iterator<String> iterator = linkedList.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } for (int i = 0 ; i < linkedList.size(); i++) { System.out.println(linkedList.get(i)); } } }
可以看到底层还是通过iterator来拿到的数据,并不是索引
这几个方法就pop和push稀罕一点,其他的就不演示了,但是其底层还是removeFirst和addFirst。瞬间索然无味
LinkedList底层成员 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 transient int size = 0 ; transient Node<E> first; transient Node<E> last; Node代表的是节点对象 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 } }
分析add方法 创建第一个
再来一个
分析get方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private void checkPositionIndex (int index) { if (!isPositionIndex(index)) throw new IndexOutOfBoundsException (outOfBoundsMsg(index)); } Node<E> node (int index) { if (index < (size >> 1 )) { Node<E> x = first; for (int i = 0 ; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1 ; i > index; i--) x = x.prev; return x; } }
虽然说慢,但是给出了优化查找的方法
index < (size >> 1) 采用二分思想,先将index与长度size的一半比较,如果index < size/2,就从位置0往后遍历到位置index处,而如果index > size/2,就只从位置size往前遍历到位置index处,这样可以减少一部分不必要的遍历
这并==不是==二分法查询,但是利用了二分思想
增强for 1 2 3 4 5 6 7 8 基本作用:遍历集合或者数组 格式: for(元素类型 变量名:要遍历的集合名或者数组名){ 变量名代表每一个元素 } 快捷键: 集合名或者数组名.for
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Demo05for { public static void main (String[] args) { ArrayList<String> list = new ArrayList <>(); list.add("张三" ); list.add("二柱" ); list.add("赵六" ); list.add("王五" ); list.add("李四" ); for (String s : list) { System.out.println("s = " + s); } int [] arr = {1 ,2 ,3 ,4 ,5 ,6 ,7 }; for (int i : arr) { System.out.println("i = " + i); } } }
注意:
增强for遍历集合时,底层原理是迭代器(因此遍历时不能更改集合长度)
增强for遍历数组时,底层原理是普通for
因此在使用增强for的时候,还是要注意不要触发并发修改异常
==Collections==集合工具类 1 2 3 4 5 6 7 8 概述:集合工具类 特点:构造私有、方法静态 方法: static <T> boolean addAll(Collection<? super Y=T> c, T…… element) -> 批量添加元素 static void shuffle(List<?> list) -> 将集合的元素顺序打乱 static <T> void sort(List<T> list) -> 将集合中的元素按照默认规则排序(ASCII码表) static <T> void sort(List<T> list, Comparator<? super T> c) -> 将集合中的元素按照指定规则排序
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Demo01 { public static void main (String[] args) { ArrayList<String> list = new ArrayList <>(); Collections.addAll(list,"张三" ,"李四" ,"王五" ,"二柱" ,"赵六" ); System.out.println("list = " + list); Collections.shuffle(list); System.out.println("list = " + list); Collections.sort(list); System.out.println("list = " + list); } }
shuffle打乱顺序是随机打乱的,每次运行都不同
sort默认排序是按照ASCII码表排序的,将指定规则拉出来单独说一下
1 2 3 4 5 6 7 static <T> void sort(List<T> list, Comparator<? super T> c) -> 将集合中的元素按照指定规则排序 Comparator比较器 方法: int compare(T o1, T o2) o1-o2 -> 升序 o2-o1 -> 降序
示例一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Demo02 { public static void main (String[] args) { ArrayList<Person> list = new ArrayList <>(); list.add(new Person (99 ,"张三" )); list.add(new Person (39 ,"李四" )); list.add(new Person (29 ,"王五" )); list.add(new Person (69 ,"二柱" )); Collections.sort(list, new Comparator <Person>() { @Override public int compare (Person o1, Person o2) { return o1.getAge()- o2.getAge(); } }); System.out.println("list = " + list); } }
Person类是手动定义的,也不用看代码了,大部分是快捷键生成的
注意添加对象的时候不能直接添加,需要new,进行一个有参构造
这里的排序规则使用了匿名内部类,重写了compare方法,使用了年龄的升序排序
为啥,默认会是ASCII码排序呢,因为底层实现了一个Comparable接口
1 2 接口:Comparable接口 方法:int compareTo(T o) -> this-o(升序) o-this(降序)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class PersonTest implements Comparable <PersonTest>{ String name; int age; …………………………………… @Override public int compareTo (PersonTest o) { return this .getAge()-o.getAge(); } }
1 2 >Arrays中的静态方法 static <T> List<T> asList(T……a) -> 直接指定元素,转存到list集合中
1 >List<String> list = Arrays.asList("张三","李四","王五");
泛型 1 2 3 4 5 6 7 泛型:<> 作用: 统一数据类型,防止将来的数据转换异常 注意: 1、泛型中的类型必须是引用数据类型 2、如果不写泛型,默认Object型
1 2 从使用层面来说,防止数据类型转换异常 从定义层面来说,定义带泛型的类,方法等,将来使用的时候给泛型确定什么类型,泛型就会变成什么类型,凡是涉及到泛型的都会变成确定的类型,代码更灵活
含有泛型的类 1 2 3 4 5 6 格式: public class 类名<E>{ } 含有泛型的类是new对象的时候确定类型的,参考ArrayList
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Demo001fx <E> { private int size; private Object[] obj = new Object [10 ]; public boolean add (E e) { obj[size] = e; size++; return true ; } public E get (int index) { return (E) obj[index]; } @Override public String toString () { return Arrays.toString(obj); } }
含有泛型的方法 1 2 3 4 5 6 格式: 修饰符 <E> 返回值类型 方法名(E e){ } 含有泛型的方法在调用的时候确定类型
1 2 3 4 5 6 7 8 9 10 public class Demo03addAll { public static <E> void addAll (ArrayList<E> list, E ... e) { for (E element : list) { list.add(element); } } }
1 2 3 4 5 6 7 8 9 10 public class Demo03 { public static void main (String[] args) { ArrayList<String> list = new ArrayList <>(); Demo03addAll.addAll(list,"a" ,"b" ); } }
含有泛型的接口 1 2 3 4 5 6 7 8 格式: public interface 接口名<E>{ } 确定泛型类型: 1、在实现类的时候还没有确定类型,在new实现类的时候确定类型 -- ArrayList 2、在实现类的时候就直接确定类型了 -- Scanner
定义接口
1 2 3 4 5 public interface myList <E>{ public boolean add (E e) ; }
ArrayList类型就不演示了,看一下在实现类确定类型的,Scnner
也不用演示了,这里就可以看到,在实现接口的时候确定了类型
泛型的通配符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Test { public static void main (String[] args) { ArrayList<String> list1= new ArrayList <>(); list1.add("张三" ); list1.add("李四" ); ArrayList<Integer> list2 = new ArrayList <>(); list2.add(1 );list2.add(2 ); method(list1); method(list2); } public static void method (ArrayList<?> list) { for (Object o: list){ System.out.println(o); } } }
这里的method方法就用到了通配符 ?
但是这种使用比较基础,不写<?>,直接写ArrayList也是可以运行的,整点狠活
泛型的上限下限 1 2 3 4 5 6 7 8 9 作用:可以规定泛型的范围 上限: 格式:<? extends 类型> 含义:?只能接收extends后面的本类型以及子类类型 下限: 格式:<? super 类型> 含义:?只能接收super后面的本类类型和父类类型
使用这个需要搞清楚类型的子父类关系
应用场景:
1、如果我们在定义类、方法、接口的时候,如果类型不确定,我们可以考虑定义含有泛型的类、方法、接口
2、如果类型不确定,但是能知道以后只能传递某个类的继承体系中的子类或者父类,就可以使用泛型通配符
学以致用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Demo { private static List<String> color = Arrays.asList("♥" ,"♣" ,"♦" ,"♠" ); private static List<String> num = Arrays.asList("A" ,"2" ,"3" ,"4" ,"5" ,"6" ,"7" ,"8" ,"9" ,"10" ,"J" ,"Q" ,"K" ); private static ArrayList<String> obj = new ArrayList <>(); public static ArrayList get () { for (int i = 0 ; i < color.size(); i++) { for (int i1 = 0 ; i1 < num.size(); i1++) { obj.add(color.get(i)+num.get(i1)); } } obj.add("大王" ); obj.add("小王" ); return obj; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public class Test { public static void main (String[] args) { Demo demo = new Demo (); ArrayList<String> list = demo.get(); Collections.shuffle(list); ArrayList<String> p1 = new ArrayList <>(); ArrayList<String> p2 = new ArrayList <>(); ArrayList<String> p3 = new ArrayList <>(); ArrayList<ArrayList<String>> rom = new ArrayList <>(List.of(p1,p2,p3)); ArrayList<String> last = new ArrayList <>(); last.add(list.remove(53 )); last.add(list.remove(52 )); last.add(list.remove(51 )); System.out.println(last); for (int i = 0 ; i < list.size(); i++) { if (i%3 ==0 || i==0 ){ p1.add(list.get(i)); } if (i%3 ==1 ) { p2.add(list.get(i)); } if (i%3 ==2 ){ p3.add(list.get(i)); } } Random num = new Random (); int data = num.nextInt(3 ); System.out.println(data); System.out.println("请" +rom.get(data)+"玩家选择是否当地主" ); Scanner sc = new Scanner (System.in); while (true ){ System.out.println("请输入Y或是F" ); String content = sc.next(); if (content.equals("Y" ) || content.equals("y" )) { for (int i = 0 ; i < last.size(); i++) { rom.get(data).add(last.get(i)); } break ; } else if (content.equals("F" ) || content.equals("f" )) { if (data<3 ){ data++; } else { data = 0 ; } } else { System.out.println("非法输入,请重新输入" ); } } } }
这样实现了,斗地主的一个牌面生成,取出底牌,发牌以及获取底牌的过程,这只是对学的集合知识简单的应用,并没有要开发游戏,不过闲的没事可以试试看。
这个生成牌面的过程也可以使用数组来解决,都差不多
1 2 String[] color = "♠-♣-♦-♥" .split("-" ); String[] number = "A-2-3-4-5-6-7-8-9-10-J-Q-K" .split("-" );
红黑树(了解)
先说一下排列树:左子树小,右子树大
红黑树规则:
1、每个节点必须是红色或者黑色的
2、根节点必须是黑色
3、如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的
4、如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
5、对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
红黑树趋近于平衡树,而且数据存储时按照排序树规则存储,查询速度快
https://www.cs.usfca.edu/~galles/visualization/RedBlack
一个演示红黑树的网站
1 2 3 4 5 集合加入红黑树的目的:提高查询效率 HashSet集合: 数据结构:哈希表 jdk8之前:哈希表 = 数组+链表 jdk8之后:哈希表 = 数组+链表+红黑树 -> 目的是提高查询效率
Set接口 1 set接口并没有对collection接口进行功能上的扩充,而且所有的Set集合底层都是依靠Map实现
set的所有方法,点开都是map
1 2 set和Map是密切相关的 遍历Map需要先变成单列集合,只能变成set集合
HashSet集合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 概述:是set的实现类 特点: 1、元素唯一 2、元素无序 3、无索引 4、线程不安全 数据结构:哈希表 jdk8之前:哈希表 = 数组+链表 jdk8之后:哈希表 = 数组+链表+红黑树 方法:和collection一模一样 遍历: 1、增强for(没有普通for因为不能操作索引) 2、迭代器
这里的无序只是和插入的顺序不同,HashSet是将数据插入之后,利用方法根据哈希表生成一个值,然后根据这个值的大小排序,以链表的方式存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class Demo { public static void main (String[] args) { HashSet<String> hashSet = new HashSet <>(); hashSet.add("张三" ); hashSet.add("李四" ); hashSet.add("王五" ); hashSet.add("赵六" ); hashSet.add("二柱" ); hashSet.add("赵四" ); hashSet.add("刘能" ); hashSet.add("我" ); hashSet.add("他" ); System.out.println("hashSet = " + hashSet); Iterator<String> iterator = hashSet.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } for (String s : hashSet) { System.out.println("s = " + s); } } }
LinkedHahSet 1 2 3 4 5 6 7 8 9 HashSet的子类 特点: 1、元素唯一 2、元素有序 3、无索引 4、线程不安全 数据结构:哈希表+双向链表 使用:和HashSet一样
哈希值 1 2 3 概述:是由计算机算出来的一个十进制数,可以看做是对象的地址值 获取对象的哈希值,使用的是Object中的方法 public native int hashCode()
哈希值不一样,内容肯定不一样,但是哈希值一样,内容可能不一样哦
一个特殊案例,“通话”和“重地”哈希值是一样的
研究一下String重写的hashCode哈希计算源码,看一下 abc
的哈希值是怎么计算出来的
1 2 3 4 String s1 = "abc" ;byte [] value = {97 ,98 ,99 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public int hashCode () { int h = hash; if (h == 0 && !hashIsZero) { h = isLatin1() ? StringLatin1.hashCode(value) : StringUTF16.hashCode(value); if (h == 0 ) { hashIsZero = true ; } else { hash = h; } } return h; } ============================== StringLatin1.hashCode(value)底层源码 public static int hashCode (byte [] value) { int h = 0 ; for (byte v : value) { h = 31 * h + (v & 0xff ); } return h; }
这里的 oxff
是十六进制的255,任何数据和255做与运算都是原值
1 2 3 4 5 6 7 8 第一圈: h = 31 * 0 + 97 (97) 第二圈: h = 31 * 97(h) + 98 (3105) 第三圈: h = 31 * 3105 + 99 (96354)
在计算哈希的时候,有一个定值31,为啥
31是一个质数,31可以尽量降低内容不同但是哈希值一样的情况(哈希冲突,哈希碰撞)
这是一个统计学问题
HashSet存储去重过程 1 2 3 4 5 先计算元素的哈希值(重写hashCode方法),再比较内容(重写equals方法) 过程: 先比较哈希值,如果哈希值不一样,存储 如果哈希值一样,比较内容,内容不同存储,内容相同,去重
如果hashSet存储自定义类型数据,就没有去重效果了,因为地址值一定不同,还想要有去重效果,就需要重写hashCode和equals
1 2 3 4 5 6 7 8 9 10 11 12 @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; Person person = (Person) o; return Objects.equals(name, person.name) && Objects.equals(age, person.age); } @Override public int hashCode () { return Objects.hash(name, age); }
总结:
1、如果HashSet存储自定义数据类型,需要重写hashcode和equals方法,让HashSet比较属性的哈希值以及属性的内容
2、如果不重写hashCode和equals方法,默认调用的是Object中的方法,不同对象,哈希值一定不一样,就不能去重了
双列集合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 map是双列集合的顶级接口 map的实现类HashMap: 特点: 1、key唯一,value可重复 2、无序 3、无索引 4、线程不安全 5、可以存null键,null值 数据结构:哈希表 HashMap的子类LinkedHashMap: 特点: 1、key唯一,value可重复 2、有序 3、无索引 4、线程不安全 5、可以存null键,null值 数据结构:哈希表+双向链表 map的实现类Hashtable 特点: 1、key唯一,value可重复 2、无序 3、无索引 4、线程安全 5、不可以存null键,null值 数据结构:哈希表 Hashtable的子类Properties(主要和配置文件结合使用) 特点: 1、key唯一,value可重复 2、无序 3、无索引 4、线程安全 5、可以存null键,null值 6、key和value都是String型的 数据结构:哈希表 map的实现类TreeMap: 特点: 1、key唯一,value可重复 2、可以对key进行排序 3、无索引 4、线程不安全 5、不能存null键,null值 数据结构:红黑树
Map接口 1 2 3 概述:双列集合的顶级接口 元素特点: 元素由键值对组成(key-value)
HashMap 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 概述:Map的实现类 特点: 1、key唯一,value可以重复 -> 如果key重复,会发生value覆盖 2、无序(针对整数,不会随意打乱顺序) 3、无索引 4、线程不安全 5、可以存null键,null值 数据结构:哈希表 // PS:感觉像python里面的字典 方法: V put(K key, V value) -> 添加元素,返回的是被覆盖的value V remove(Object key) -> 根据key删除键值对,返回的是被删除的键值对的值(value) V get(Object key) -> 根据key获取value boolean containsKey(Object key) -> 判断集合中是否包含指定的key Collection<V> values() -> 获取集合中所有的value,转存到Collection集合中 Set<V> keySet() -> 将Map中的key获取出来,转存到Set集合中 Set<Map, Entry<K, V>> entrySet() -> 获取Map集合中的键值对,转存到Set集合中 注意,因为是键值对,因此新建集合是要传入两个泛型,对应key - value
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Demo { public static void main (String[] args) { HashMap<String,String> hashMap = new HashMap <>(); String s1 = hashMap.put("张三" ,"18" ); System.out.println("s1 = " + s1); String s2 = hashMap.put("张三" ,"28" ); System.out.println("s2 = " + s2); hashMap.put("李四" ,"38" ); hashMap.put("王五" ,"48" ); hashMap.put("" ,"1" ); hashMap.put(null ,null ); String s3 = hashMap.remove("" ); System.out.println("s3 = " + s3); System.out.println(hashMap.get("李四" )); System.out.println(hashMap.containsKey("" )); Collection<String> c1 = hashMap.values(); System.out.println(c1); } }
LinkedHashMap 1 2 3 4 5 6 7 8 9 10 概述:HashMap的子集 特点: 1、key唯一,value可以重复 -> 如果key重复,会发生value覆盖 2、有序 3、无索引 4、线程不安全 5、可以存null键,null值 数据结构:哈希表+双向链表 方法:和HashMap一样
遍历的两种方式 1 2 3 4 5 遍历就需要用到没有演示的两个方法了 Set<V> keySet() -> 将Map中的key获取出来,转存到Set集合中 Set<Map, Entry<K, V>> entrySet() -> 获取Map集合中的键值对,转存到Set集合中 之前说过,Map是没法遍历的,需要转换成set
第一种方式
1 2 3 4 Set<String> set = hashMap.keySet(); for (String s : set) { System.out.println(s + " = " + hashMap.get(s)); }
先获取所有的key,遍历key,再查找到对应的value
第二种方法
1 2 3 4 5 6 7 Set<Map.Entry<String,String>> set1 = hashMap.entrySet(); System.out.println("set1 = " + set1); for (Map.Entry<String, String> entry : set1) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key+"..." +value); }
获取记录key和value的对象,Map接口中的静态内部接口:Map.Entry
调用Map.Entry中的两个方法
getKey() getValue()
方法就顾名思义吧
Map去重 1 2 3 set执行add的时候,首先就是map.put(e, PRESENT) 把要插入的值作为key,而进行对象类型去重时,则是需要重写hashCode和equals方法 Map对象去重和set一样,都是重写两个方法,原因就是Set的值是保存在Map的key的位置的
Map练习 1 2 3 4 5 6 7 8 经典python二级,字典统计字词个数 步骤: 1、创建HashMap 2、遍历字符串,取出每个字母 3、判断map中是否有该字母 4、如果没有,直接存储,如果有获取对应的value,然后加一 5、重新保存到集合
统计输入的字母个数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class fori { public static void main (String[] args) { HashMap<String, Integer> hashMap = new HashMap <>(); Scanner sc = new Scanner (System.in); String s1 = sc.next(); char [] c1 = s1.toCharArray(); for (char c : c1) { if (!hashMap.containsKey(c)) { hashMap.put("c" ,1 ); } else { Integer value = hashMap.get(c); value++; hashMap.put(c+"" ,value); } } System.out.println("hashMap = " + hashMap); } }
这个东西让我想到了备考python二级时经常碰见的东西
1 2 3 4 5 6 for i in range (len (tem)): d[tem[i]] = d.get([tem[i]] , 0 ) + 1 //用于统计数据 d.sort(key=lambda x:x[1 ] , value=True ) //按照升序排序
所以我感觉应该有精简的方法,于是就问了一把AI
1 2 3 4 5 6 7 8 9 10 11 public static void main (String[] args) { HashMap<String, Integer> hashMap = new HashMap <>(); Scanner sc = new Scanner (System.in); String s1 = sc.next(); char [] c1 = s1.toCharArray(); for (char c : c1) { hashMap.merge(c+"" , 1 , Integer::sum); } System.out.println("hashMap = " + hashMap); }
这里使用了merge方法代替了if-else
merge()方法接受三个参数:键、需要合并的值、以及一个函数
c+“” -> 因为定义的是字符串,c是char类型,简单转换一下
1 -> 这个是要操作的值,为1
Integer::sum -> 顾名思义了直接,就是数字求和
这行代码的作用和上面是一样的,如果键不存在,插入键值对,并将值设为1,如果存在+1
继续操作一下之前的那个斗地主,之前是利用ArrayList集合来实现的,但是还没有排序,扑克牌是用一个符号和数字组成,如果说再拆开排序不是不行,感觉很笨。因为map的key数字是不会打乱的,我们就可以对这些字符串按照牌面的大小,由数字进行一个排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 public class Demo { public static void main (String[] args) { HashMap<Integer, String> map = new HashMap <>(); map.put(0 , "大王" ); map.put(1 , "小王" ); String[] color = "♠-♣-♦-♥" .split("-" ); String[] number = "2-A-K-Q-J-10-9-8-7-6-5-4-3" .split("-" ); int j = 2 ; for (int i1 = 0 ; i1 < number.length; i1++) { for (int i = 0 ; i < color.length; i++) { map.put(j, color[i] + number[i1]); j++; } } System.out.println("map = " + map); ArrayList<Integer> list = new ArrayList <>(); for (int i = 0 ; i < 54 ; i++) { list.add(i); } Collections.shuffle(list); ArrayList<Integer> p1 = new ArrayList <>(); ArrayList<Integer> p2 = new ArrayList <>(); ArrayList<Integer> p3 = new ArrayList <>(); ArrayList<Integer> last = new ArrayList <>(); last.add(list.remove(53 )); last.add(list.remove(52 )); last.add(list.remove(51 )); System.out.println(last); for (int i = 0 ; i < list.size(); i++) { if (i % 3 == 0 ) { p1.add(list.get(i)); } if (i % 3 == 1 ) { p2.add(list.get(i)); } if (i % 3 == 2 ) { p3.add(list.get(i)); } } Collections.sort(p1); Collections.sort(p2); Collections.sort(p3); String s1 = lookPoker(p1,map); String s2 = lookPoker(p2,map); String s3 = lookPoker(p3,map); ArrayList<ArrayList<Integer>> rom = new ArrayList <>(List.of(p1,p2,p3)); Random num = new Random (); int data = num.nextInt(3 ); System.out.println("请" + rom.get(data) + "玩家选择是否当地主" ); Scanner sc = new Scanner (System.in); while (true ) { System.out.println("请输入Y或是F" ); String content = sc.next(); if (content.equals("Y" ) || content.equals("y" )) { for (int i = 0 ; i < last.size(); i++) { rom.get(data).add(last.get(i)); } break ; } else if (content.equals("F" ) || content.equals("f" )) { if (data < 3 ) { data++; } else { data = 0 ; } } else { System.out.println("非法输入,请重新输入" ); } } } public static String lookPoker (ArrayList<Integer> list, HashMap<Integer, String> map) { StringBuilder e = new StringBuilder (); for (Integer key : list) { String value = map.get(key); e.append(value); } String s = e.toString(); return s; } }
这个程序写的还是有瑕疵的,因为使用了字符串来返回手牌情况,考虑到后面获取底牌和出牌,字符串并不是很好操作
有时间继续优化
哈希表存储的过程 1 2 3 4 哈希表存储数据去重复的过程 1、先比较元素的哈希值hashCode,再比较内容equals 2、如果哈希值不一样,存 3、如果内容不一样,存
计算出 通话
和 重地
的存储位置相同,这个时候两者之间产生了链表关系,jdk版本不同,jdk8之前数据关系是 哈希表 = 数组+链表
,jdk8之后是 哈希表 = 数组+链表+红黑树
了。
同一存储位置,数据过多时会变成红黑树。加快查询速度
注意:
1、哈希表中的数组默认长度为==16==,但是是第一次put的时候数组才会被初始化为长度为16的数组(和ArrayList中的add一样)
2、哈希表中有一个加载因子:==0.75F==
含义是:数组存储达到百分之七十五的时候,扩容
==扩容两倍==
3、如果链表长度达到==8==,并且数组容量大于等于==64==的时候,链表自动转成红黑树
4、如果删除元素,元素个数小于等于==6==,红黑树自动转回链表
1 2 3 4 5 default_initial_capacity:HashMap默认容量 16 default_1oad_factor:HashMap默认加载因子 0.75F thresho1d:扩容的临界值 等于 容量*0.75=12 第一次扩容 treeify_thresho1d:链表长度默认值,转为红黑树:8 min_treeify_capacity:链表被树化时最小的数组容量:64
哈希表底层源码
举的例子不是很经典,下面换成 abc
通话
重地
可以看到数组长度小于64的时候不进行树化,先对数组进行扩容。下面的else-if是树化的代码
从底层代码了解了一下数据存储的过程,我们也可以手动指定加载因子和容量,也就是有参构造
1 HashMap<String,String> map = new HashMap<>(5,0.5F);
1 2 3 4 static final int tableSizeFor (int cap) { int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1 ); return (n < 0 ) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1 ; }
tableSizeFor方法,怎么说呢,就是你传入的数字,并不是按你的意愿创造这个大小的容量的,实测,容量大小为大于等于你传入数字的2的倍数
8 4 2 1 规则
目的是为了尽量减少哈希碰撞
索引问题 1 2 3 4 5 6 7 8 问题1:哈希表的组成明明有索引,但是为什么set和map中没有呢 哈希表中虽然有索引,但是set和map却没有索引,因为哈希表还有链表的存在,存数据的时候有可能在同一索引下形成链表,这个时候去去索引,就会出现问题,因为不是一一对应的,不知道该取出哪一个 问题2:为啥说HashMap是无序的,LinkedHashMap是有序的呢? HashMap底层哈希表为单向链表 LinkedHashMap底层在哈希表的基础上加了一条双向链表
单向链表:==从索引0开始往后一条链表一条链表的遍历==
HashMap,数据存储时有存储位置,,而且存储位置可能是同一个,这样遍历的时候从0索引开始向后遍历,而不是按照存储的顺序遍历,如果遇到一个位置上有多个元素的情况,先把这条链表遍历完,再进入下一个位置,这样就导致了无序,但其实遍历出来的东西还有有一定的顺序的,并不是完全意义上的无序
双向链表:从第一个节点开始遍历,以此遍历后面的元素
这个和双向链表和for循环顺序有关系,因为双向链表是前后相连的。
TreeSet 1 2 3 4 5 6 7 8 9 概述:是set的实现类 特点: 1、对元素进行排序 2、无索引 3、不能存null 4、线程不安全 5、元素唯一 数据结构:红黑树
1 2 3 构造方法: TreeSet() -> 构造一个新的空set,该set根据其元素的自然顺序进行排序 -> ASCII TreeSet(Comparator<? super E> comparator) -> 构造一个空TreeSet,它根据比较器进行排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Test { public static void main (String[] args) { TreeSet<String> treeSet = new TreeSet <>(); treeSet.add("c.qwe" ); treeSet.add("a.qwe" ); treeSet.add("d.qwe" ); treeSet.add("b.qwe" ); System.out.println("treeSet = " + treeSet); System.out.println("============================" ); TreeSet<person> set = new TreeSet <>(new Comparator <person>() { @Override public int compare (person o1, person o2) { return o1.getAge()- o2.getAge(); } }); set.add(new person ("张三" ,18 )); set.add(new person ("二柱" ,27 )); set.add(new person ("李四" ,72 )); set.add(new person ("张麻子" ,19 )); System.out.println("set = " + set); } }
TreeMap 1 2 3 4 5 6 7 8 9 概述:map的实现类 特点: 1、对key进行排序 2、无索引 3、key唯一 4、线程不安全 5、不能存null 数据结构:红黑树
1 2 3 构造方法: TreeMap() -> 使用键的自然顺序构造一个新的、空的树映射 -> ASCII TreeMap(Comparator<? super E> comparator) -> 构造一个,新的、空的树映射,该映射根据给定比较器进行排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Demo { public static void main (String[] args) { TreeMap<String, String> treeMap = new TreeMap <>(); treeMap.put("c" ,"qwe" ); treeMap.put("a" ,"qwe" ); treeMap.put("d" ,"qwe" ); treeMap.put("b" ,"qwe" ); System.out.println("treeMap = " + treeMap); System.out.println("============================" ); TreeMap<person, String> map = new TreeMap <>(new Comparator <person>() { @Override public int compare (person o1, person o2) { return o1.getAge()-o2.getAge(); } }); map.put(new person ("张三" ,18 ), "上海" ); map.put(new person ("二柱" ,27 ), "上海" ); map.put(new person ("李四" ,72 ), "上海" ); map.put(new person ("张麻子" ,19 ), "北京" ); System.out.println("map = " + map); } }
Hashtable和Vector(了解) Hashtable 1 2 3 4 5 6 7 8 9 map的实现类 特点: 1、key唯一,value可重复 2、无序 3、无索引 4、线程安全 5、不能存null键,null值 数据类型:哈希表
HashMap和Hashtable的区别:
相同点:元素无序,无索引,key唯一
不同点:HashMap线程不安全,Hashtable线程安全
HashMap可以存储null键null值,Hashtable不能
Hashtable的一些具体方法和HashMap一样
Vector集合 1 2 3 4 5 6 7 8 9 10 11 概述:list接口的实现类 特点: 1、元素有序 2、有索引 3、元素可重复 4、线程安全 数据结构:数组 如果用空参构造创建对象,数组初始容量为10,自动扩容2倍 如果初始容量和容量增量,扩容就是现有容量+容量增量
使用方式和ArrayList相似
源码分析:
1 2 Vector() 构造一个空向量,使其内部数据数组的大小为10,其标准容量增量为零 Vector(int initialcapacity, int capacityIncrement) 使用指定的初始容量和容量增量构造一个空的向量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public Vector () { this (10 ); } public Vector (int initialCapacity) { this (initialCapacity, 0 ); } public Vector (int initialCapacity, int capacityIncrement) { super (); if (initialCapacity < 0 ) throw new IllegalArgumentException ("Illegal Capacity: " + initialCapacity); this .elementData = new Object [initialCapacity]; this .capacityIncrement = capacityIncrement; }
这一点就是和ArrayList不一样的点,ArrayList对象new出来长度为0,只要第一个add的时候才会扩容到10,而Vector新new出来的容量就是10
看一下扩容的具体代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public synchronized boolean add (E e) { modCount++; add(e, elementData, elementCount); return true ; } private void add (E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; elementCount = s + 1 ; } private Object[] grow() { return grow(elementCount + 1 ); } private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, capacityIncrement > 0 ? capacityIncrement : oldCapacity ); return elementData = Arrays.copyOf(elementData, newCapacity); }
再来看有参构造
指定容量增量为5之后,再次扩容就会扩容到15
Properties集合(属性集) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 概述:Hashtable的子类 特点: 1、key唯一,value可重复 2、无序 3、无索引 4、线程安全 5、不能存null键,null值 6、Properties的key和value类型默认为String 数据结构:哈希表 特有方法: Properties的一套方法和Map很相近,但也有特有方法 Object setProperty(String key, String value) -> 存键值对 String getProperty(String key) -> 根据key获取value值 Set<String> stringPropertyNames() -> 获取所有的key,保存到set集合中,相当于keySet方法 void load(InputStream inStream) -> 将流中的数据加载到Properties集合中(IO流使用,现在不说)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Proper { public static void main (String[] args) { Properties properties = new Properties (); properties.setProperty("root" ,"root" ); properties.setProperty("username" ,"root" ); properties.setProperty("123456" ,"root" ); System.out.println(properties.getProperty("root" )); Set<String> set = properties.stringPropertyNames(); for (String s : set) { System.out.println(properties.getProperty(s)); } } }
集合嵌套 1 2 3 4 5 6 7 之前写斗地主的时候,问AI就用到这个嵌套了 ArrayList<String> p1 = new ArrayList<>(); ArrayList<String> p2 = new ArrayList<>(); ArrayList<String> p3 = new ArrayList<>(); ArrayList<ArrayList<String>> rom = new ArrayList<>(List.of(p1,p2,p3));
这东西就这么存进去了,如果要遍历的话,可以遍历大集合,再遍历小集合取出元素
集合嵌套的样式有点多,但是主要的就是泛型传的对,遍历拿得出就可以了,没啥特别要说的
IO流 File类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 计算机常识: 1、以.jpg结尾的一定是图片吗? 也可能是文件夹 2、什么是文本文档 用记事本打开,人能看懂的东西(word打开就看不懂) 3、E:\Idea\io\1.jpg 中的1.jpg的父路径是谁? E:\Idea\io 这个是父路径 io 是父级文件夹 4、分隔符 a.路径名称分隔符 Windows: \ Linux: / b.路径分隔符:一个路径和其他路径之间的分隔符 ;
1 2 3 概述:文件和目录路径名的抽象表示 简单理解: 在创建File对象的时候,需要传入一个路径,这个路径定位到哪个文件夹或者文件上,我们的File就代表哪个对象
File使用 1 2 3 4 5 6 File的静态成员: static String pathSeparator:与系统有关的路径分隔符,为了方便,它被表示为一个字符串 static String separator:与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串 这俩东西的作用就是为了实现一次编写,导出运行,可以在不同操作系统中运行
1 2 3 4 5 6 7 8 9 10 11 12 File的构造方法 File(String parent, String child) 根据所填写的路径创建File对象 parent:父路径 child:子路径 File(File parent, String child) 根据所填写的路径创建File对象 parent:父路径,是一个File对象 child:子路径 File(String pathname) 根据所填写的路径创建File对象 pathname:直接指定路径
细节,传递的路径可以是不存在的,但是传递不存在的路径,没有什么意义
方法 获取方法 1 2 3 4 String getAbsolutePath() -> 获取File的绝对路径->带盘符的路径 StringgetPath() -> 获取的是封装路径->newFile对象的时候写的啥路径,获取的就是啥路径 String getName() -> 获取的是文件或者文件夹名称 long length() -> 获取的是文件的长度->文件的字节数
创建方法 1 2 3 4 5 6 7 boolean createNewFile() -> 创建文件 如果要创建的文件之前有,创建失败,返回false 如果要创建的文件之前没有,创建成功,返回true boolean mkdirs() -> 创建文件夹(目录)既可以创建多级文件夹,还可以创建单级文件夹 如果要创建的文件夹之前有,创建失败,返回false 如果要创建的文件夹之前没有,创建成功,返回true
删除方法 1 2 3 boolean delete -> 删除文件或者文件夹 1、不会移到回收站,直接删除 2、只能删除空文件夹
判断方法 1 2 3 boolean isDirectory() -> 判断是否为文件夹 boolean isFile() -> 判断是否为文件 boolean exists() -> 判断文件或者文件夹是否存在
遍历方法 1 2 3 4 5 6 7 8 9 String[] 1ist() -> 遍历指定的文件夹,返回的是string数组 File[] listFiles() -> 追历指定的文件夹,返回的是File数组 -> 这个推荐使用 listFiles方法底层还是list方法 调用list方法,遍历文件夹,返回一个String数组,遍历数组,将数组中的内容一个一个封装到File对象中,然后再将File对象放到File数组中 注意: 遍历文件夹,只会遍历文件夹内的文件,如果还有子级文件夹不会遍历,但是会遍历出文件夹名
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class Test02 { public static void main (String[] args) throws IOException { File f1 = new File ("D:\\新桌面" ,"IO" ); File f2 = new File (new File ("D:\\新桌面" ),"IO" ); File f3 = new File ("D:\\新桌面\\IO\\1.txt" ); System.out.println(f1.getAbsoluteFile()); System.out.println(f2.getPath()); System.out.println(f1.getName()); System.out.println(f3.length()); File f4 = new File ("D:\\新桌面\\IO\\10.txt" ); System.out.println(f4.createNewFile()); File f5 = new File ("D:\\新桌面\\IO\\bb" ); System.out.println(f5.mkdirs()); System.out.println(f4.delete()); System.out.println(f5.isDirectory()); System.out.println(f5.isFile()); System.out.println(f4.exists()); String[] s1 = f1.list(); for (String s : s1) { System.out.println("s = " + s); } File[] s2 = f1.listFiles(); for (File file : s2) { System.out.println("file = " + file); } } }
小练习,遍历目录下的所有txt文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Test03 { public static void main (String[] args) { File f1 = new File ("D:\\新桌面\\IO" ); method(f1); } public static void method (File f1) { File[] f2 = f1.listFiles(); for (File file : f2) { if (file.isFile()) { String name = file.getName(); if (name.endsWith(".txt" )) { System.out.println(name); } } else { method(file); } } } }
说一下相对路径的写法,要从项目的根目录下面开始写,因为如果直接就创建一个1.txt这个时候文件是生成在项目的根目录下的和out目录同级。
字节流 IO流介绍 1 2 3 4 5 6 7 8 9 10 11 12 单词: Output:输出 Input:输入 write:写入 read:读取 IO流: 将一个设备上的数据传输到另一个设备上,称为IO流技术 学IO流的目的: 集合和数组都能存储数据,但是这两个都是临时存储(代码运行完毕,集合和数组就从内存中释放了,数据也就不存在了),所以集合和数组不能永久保存数据。这个时候就需要IO流,将数据保存到文件里
IO流的流向 1 2 3 4 5 6 7 针对于se阶段的IO 输入流:将数据从硬盘上读到内存中 Input 输出流:从内存出发,将数据写到硬盘上 Output 要是从电脑和电脑之间做数据传输,就是相对的 发数据的一方:输出 收数据的一方:输入
IO流分类 1 2 3 4 5 6 字节流:万能流,一切皆字节 字节输出流: OutputStream 抽象类 字节输入流: InputStream 抽象类 字符流:专门操作文本文档 字符输出流: Writer 抽象类 字符输入流: Reader 抽象类
字节输出流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 概述:OutputStream 抽象类 子类:FileOutputStream 作用:往硬盘上写数据 构造: FileOutputStream(File file); FileOutputStream(String name) 特点: 1、指定的文件如果没有,会自动创建 2、如果有这个文件,会覆盖掉 // python的open("w")模式 方法: void write(int b) 一次写一个字节 void write(byte[] b) 一次写一个字节数组 void write(byte[] b,int off,int len) 一次写一个字节数组一部分 b:写的数组 off:从数组的哪个索引开始写 len:写多少个 void close() -> 手动关闭 注意:写是按照字节写的,保存到文件中,用记事本打开,写的数字就会按照ASCII码表翻译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Demo01 { public static void main (String[] args) throws IOException { method01(); method02(); method03(); } private static void method01 () throws IOException { FileOutputStream fos = new FileOutputStream ("mondeule01\\1.txt" ); fos.write(97 ); fos.close(); } private static void method02 () throws IOException { FileOutputStream fos = new FileOutputStream ("mondeule01\\1.txt" ); byte [] by = {97 ,98 ,99 ,100 ,101 }; fos.write(by); fos.close(); } private static void method03 () throws IOException { FileOutputStream fos = new FileOutputStream ("mondeule01\\1.txt" ); byte [] by = {97 ,98 ,99 ,100 ,101 }; fos.write(by,0 ,2 ); fos.close(); } }
这样的方法多少有点原始人了,想点流弊的,字符串有一个方法,可以转成byte类型,用它就行了
1 fos.write("abcde" .getBytes());
追加模式 1 2 3 4 5 6 7 8 FileoutputStream(String name,boolean append) append等于true -> 实现续写追加 换行: 1、windows:\r\n 或者 \n -> 占两个字节 2、Linux:\n 3、mac os:\r
字节输入流(只读模式) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 概述:InputStream 抽象类 子类:FileInputStream 作用:读数据,将数据从硬盘上读到内存中来 构造: FileInputStream(File file) FileInputStream(String path) 方法: int read() 一次读一个字节,返回的是读取的字节 int read(byte[] b) 一次读取一个字节数组,返回的是读取的字节个数 int read(byte[] b, int off, int len) 一次读取一个字节数组的一部分,返回的是读取的字节个数 void close() 关闭资源文件
一次只读一个 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private static void read01 () throws IOException { FileInputStream fis = new FileInputStream ("mondeule01\\1.txt" ); int len; while ((len = fis.read()) != -1 ){ System.out.println("len = " + len); System.out.println((char ) len); } fis.close(); }
有一个点就是,超出字节长度时再次读取也可以读取到,但是读出的数是 -1
,这个时候就利用while来读取内容了,如果遇到-1就停止,因为读出来的是ASCII码的形态,可以强转成char数组,显示字母
注意:
1、一个流对象,读完只会就不能再读了;除非再new一个新对象
2、流关闭之后,流对象不能继续使用了
读出-1问题 1 2 3 每个文件末尾都会有一个“结束标记”,这个“结束标记”看不见,摸不着 而read()方法规定,如果读取到了文件的结束标记,方法返回-1 如果文本中本来有-1,也会拆开出现,先是-再是1
一次读一个数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class Demo02 { public static void main (String[] args) throws IOException { FileInputStream fis = new FileInputStream ("mondeule01\\1.txt" ); byte [] bytes = new byte [2 ]; int len; while ((len = fis.read(bytes)) != -1 ){ System.out.println(new String (bytes, 0 ,len)); } } }
注意这里之所以使用长度为2的数组是想说明一个问题,读取完时候输出成字符串形式的时候
1 System.out.println(new String(bytes, 0 ,len));
一定要使用len来限制长度,因为如果上一个读出cd,这一从读取两个,只拿到了e,这个时候e将c覆盖掉,但是d还在哪里,这个时候输出整个数组的话,就会出现问题,也是一个小细节
实现文件复制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 复制一个图片吧 public class Demo03 { public static void main (String[] args) throws IOException { FileInputStream fis = new FileInputStream ("D:\\新桌面\\IO\\1.jpg" ); FileOutputStream fos = new FileOutputStream ("D:\\新桌面\\IO\\1plus版.jpg" ); byte [] bytes = new byte [1024 ]; int len; while ((len = fis.read(bytes)) != -1 ){ fos.write(bytes); } fos.close(); fis.close(); } }
字符流 1 2 3 4 5 上面学习字节流的时候并没有用中文,因为涉及中文编码占字节数问题,一个中午在utf-8内占3个字节,如果还是上面的读取俩字节输出一下子,指定乱码,需要读取3个字节,虽然是没问题,因为字节流是万能流,但是有点麻烦,字节流更偏向于文件复制,因此不要边度边看 但是呢,使用字符操作编码也要保持一致,否则仍然会乱码 字符流在编码一致的情况下边读边看是没啥问题的 字节流即使在编码一致的情况下,边读边看仍然可能出现问题
FileReader(字符输入流) 1 2 3 4 5 6 7 8 9 10 11 12 13 字符流是专门用来操作文本文档的,但是进行复制操作的,还是要用字节流 概述:字符出入流 -> Reader -> 是一个抽象类 作用:将文本文档中的内容读取到内存中来 构造: FileReader(File file) FileReader(String path) 方法: int read() -> 一次读取一个字符,返回的是读取字符对应的int值 int read(char[] cbuf) -> 一次读取一个字符数组,返回的是读取个数 int read(char[] cbuf, int off, int len) -> 一次读取一个字符数组一部分,返回的是读取个数 close() -> 关闭资源
FileWriter(字符输出流) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 概述:Writer 抽象类 子类:FileWriter 作用:将数据写到文件中 构造: FileWriter(File file) FileWriter(String fileName) FileWriter(String fileName, boolean addenp) -> 追加续写 方法: void writer(int c) void writer(char[] cbuf) void writer(char[] cbuf, int off, int len) void writer(String str) -> 直接写一个字符串 void close() -> 关闭资源
字符输出流,字符输入流,这俩东西和字节输出流,字节输入流方法是很相近的,就懒得演示了
有一个细节:
FileWriter底层自带一个缓冲区,我们写入的数据会先保存在缓冲区内,所以我们需要将缓冲区的数据刷到文件中。这一点和字节输出流不一样
1 2 3 4 fw.flush() // 当然直接关闭资源,也可以刷入 fw.close()
但是呢,flush只是单纯刷新,如果使用close就关闭流了,后续无法继续使用流对象了
IO流异常处理方式 1 IO操作的时候是有异常的,之前都是直接上抛,但其实Io异常处理应该try……catch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Demo04 { public static void main (String[] args) { FileReader fr = null ; try { fr = new FileReader ("mondeule01\\1.txt" ); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (fr != null ) { try { fr.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
处理异常还有专门办法
1 2 3 4 5 6 7 8 格式: try(IO对象){ 可能出现的异常代码 } catch (异常类型 对象名){ 处理异常 } 注意这个格式处理IO异常,是自动关流的
1 2 3 4 5 6 7 8 9 10 11 public class Demo04 { public static void main (String[] args) { try (FileWriter fw = new FileWriter ("mondeule\\2.txt" )) { fw.write("你好" ); } catch (Exception e){ e.printStackTrace(); } } }
缓冲流 字节缓冲流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 为啥要学字节缓冲流 之前学的FileOutputStream,FileInputStream,FileReader,Filewriter这都叫做基本类,其中FileInputStream和FileOutputStream的读写方法都是本地方法(方法声明上带native),本地方法是和系统以及硬盘打交道的,也就是说这两个对象的读和写都是在硬盘之间进行读写的,效率不高;缓冲流中底层带一个长度为8192的数组(缓冲区),此时读和写都是在内存中完成的(在缓冲区完成),内存中的读写效率非常高 使用之前需要将基本流包装成缓冲流,其实就new对象时,传递基本流 字节缓冲流: 1、BufferedOutputStream:字节缓冲输出流 构造:BufferedOutputStream(OutputStream out) 使用:和FileOutputStream一样 2、BufferedInputStream:字节缓冲输入流 构造:BufferedInputStream(FileInputStream in) 使用:和FileInputStream一样
使用缓冲流复制文件。效率比较高
细节:
问题1:使用缓冲流的时候为什么只需要关闭缓冲流,而不需要关闭基本流?
原因:缓冲流的close方法底层自动关闭基本流
问题2:缓冲流底层有数组(缓冲区),都是在内存之间进行读写,那么缓冲流读写的过程是怎么样的?
先依靠基本流将数据读出来,然后交给缓冲流,由于缓冲流缓冲区是8192,所以每次读取8192个字节放到缓冲区中,然后再将输入流缓冲区中的数据交给输出流缓冲区,然后利用基本流将数据写到硬盘上
那么在操作代码时len的作用是什么呢?len的主要作用时在两个缓冲区之间倒腾数据,将输入流缓冲区中的数据读到,然后写到输出流缓冲区中,等待输出流缓冲区满了,再依靠基本流写到硬盘上;如果输入流缓冲区中的数据读不到了,重新从硬盘读取8192个字节,进入到输出流缓冲区中,继续利用len在两个缓冲区之间倒腾数据。
字符缓冲流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 字符流的基本流底层是有缓冲区的,所以在效率这一层面不是特别明显,主要学习字符缓冲流的两个特有方法 缓冲输出流 构造: BufferedWriter(Writer w) 方法: 和FileWriter一样 特有方法: newLine() -> 换行 缓冲输入流 构造: BufferedReader(Reader r) 方法:和FileReader一样 特有方法: String readLine() -> 一次读一行,如果读到结束标记,返回null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Demo05 { public static void main (String[] args) throws IOException { BufferedWriter bw = new BufferedWriter (new FileWriter ("mondeule01\\2.txt" )); bw.write("但使龙城飞将在" ); bw.newLine(); bw.write("不教胡马度阴山" ); bw.newLine(); bw.write("杨花落尽子规啼" ); bw.close(); BufferedReader br = new BufferedReader (new FileReader ("mondeule01\\2.txt" )); String len; while ((len = br.readLine()) != null ){ System.out.println(len); } br.close(); } }
转换流 1 2 3 1.字节流读取中文在编码一致的情况,也不要边读边看,因为如果字节读不准,读不全,输出的内容有可能会出现乱码 2.所以,我们学了字符流,字符流读取文本文档中的内容如果编码一致,就不会出现乱码问题了 3.但是如果编码不一致,即使用字符流读取,仍然会出现乱码
1 2 3 4 5 6 7 8 9 10 11 概述:是字节流通向字符流的桥梁 -> 读数据 构造: InputStreamReader(InputStream in, String charsetName) charsetName:指定编码,不区分大小写 作用: 直接指定编码,按照指定编码去读取内容 用法: 基本用法和FileReader一样,因为FileReader继承InputStreamReader
这个东西相当于python中open方法的第三个参数的作用,指定编码集
OutputStreamWriter 1 2 3 4 5 字符流通向字节流的桥梁 构造: OutputStreamWriter(OutputStream out, String charsetName) 方法和FileWriter一样,因为FileWriter继承了OutputStreamWirter
1 2 3 4 5 6 7 8 9 10 11 12 public class Demo06 { public static void main (String[] args) throws IOException { OutputStreamWriter osw = new OutputStreamWriter (new FileOutputStream ("1.txt" ),"gbk" ); osw.write("你好" ); osw.close(); InputStreamReader isr = new InputStreamReader (new FileInputStream ("1.txt" ),"gbk" ); int data = isr.read(); System.out.println((char )data); isr.close(); } }
序列化流和反序列化流 1 2 3 4 5 6 7 8 9 作用:读写对象 两个对象: 1、ObjectOutputStream(序列化流) -> 写对象 2、ObjectInputStream(反序列化流) -> 读对象 注意: 我们将对象序列化到对象中,打开文件,指定看不懂,这就对了,防止数据被随意改动,只需要能读回来即可
ObjectOutputStream 1 2 3 4 5 6 作用:写对象 构造: ObjectOutputStream(OutputStream out) 方法: writeObject(Object obj) -> 写对象
1 2 3 4 5 6 作用:读对象 构造: ObjectInputStream(InputStream in) 方法: Object readObject() -> 读对象
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Demo01 { public static void main (String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("mondeule01\\person.txt" )); person p1 = new person ("张三" ,19 ); oos.writeObject(p1); oos.close(); ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("mondeule01\\person.txt" )); person p2 = (person) ois.readObject(); System.out.println(p2); ois.close(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class person implements Serializable { String name; int age; 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{" + "age=" + age + ", name='" + name + '\'' + '}' ; } }
定义对象的时候需要实现Serializable才能进行序列化。
问题:如果我有一个值,不想没序列化怎么办?
transient关键字,在不想被序列化的数据类型前面加上这个关键字即可
反序列化时出现的问题 1 2 3 问题描述: 序列化之后,修改源码,修改完之后没有重新序列化,直接反序列化了,就会出现序列号冲突问题; InvalidClassException
1 2 解决:将序列号定死,后面不管怎么修改代码,序列号都是这一个 在被序列化的对象中加上一个public static final long 的变量,并为其赋值
1 public static final long serialVersionUID = 42L ;
加上这一个就可以固定序列号,这时再更改变量的修饰符,不会影响反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class person implements Serializable { public static final long serialVersionUID = 42L ; String name; int age; 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{" + "age=" + age + ", name='" + name + '\'' + '}' ; } }
将一个对象实现一个序列化接口,将来才能让这个对象变为二进制,在网络上传输
反序列化的时候,面对多个对象肯定要使用for循环,但是注意:循环读取的次数和存储对象的个数要对应,否则会出现EOFException异常
1 2 解决方法有很多,只说一个: 创建一个ArrayList对象,将要序列化的对象全部传入集合中,然后,只需要反序列化一个集合即可,出来再for循环,集合的长度可以使用方法确定。
打印流 1 2 3 4 5 6 构造: PrintStream(String fileName) 方法: 1、println():原样输出,自带换行效果 2、print():原样输出,不换行
1 2 3 4 5 6 7 public class Demo02 { public static void main (String[] args) throws FileNotFoundException { PrintStream ps = new PrintStream ("mondeule01\\1.txt" ); ps.println("hello world" ); ps.close(); } }
改变流向 1 2 3 4 5 6 7 8 改变流向: 什么叫做改变流向: 比如:System.out.println() -> 语法本身是将语句输出到控制台 改变流向:可以让输出语句从控制台上输出改变成往指定文件中输出 方法:System中的方法: static void setOut(PrintStream out) -> 改变流向 -> 让输出语句从控制台输出转移到指定文件中
1 2 3 4 5 6 7 8 9 public class Demo02 { public static void main (String[] args) throws FileNotFoundException { PrintStream ps = new PrintStream ("mondeule01\\1.txt" ); System.setOut(ps); System.out.println("床前明月光" ); ps.close(); } }
这个时候,本该输出到控制台的语句就转存到PrintStream对象指定的文件中了,主要使用场景就是保存日志文件
打印流续写 1 PrintStream(OutputStream out) -> 可以依靠OutputStream的续写功能完成打印流续写
1 2 3 4 5 6 7 8 9 public class Demo02 { public static void main (String[] args) throws FileNotFoundException { PrintStream ps = new PrintStream (new FileOutputStream ("mondeule01\\1.txt" ,true )); System.setOut(ps); System.out.println("床前明月光" ); ps.close(); } }
Properties集合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 这个集合之前说过,可以配合IO流 回顾一下: 概述:Hashtable的子类 特点: 1、无序、无索引 2、key唯一,value可重复 3、线程安全 4、key和value的默认值都是String 特有方法: map里的方法都能使用 setProperty(String key, String value) -> 存键值对 getProperty(String key) -> 根据key获取value stringPropertyNames() -> 获取所有的key存放到set集合中 load(InputStream in) -> 将流中的数据加载到Properties集合中
使用场景:配合配置文件使用
注意:
将来我们不能将很多的硬数据放到源码中,比如用户名和密码这些数据,因为将来我们有可能换用户名或者密码,如果一换,我们就需要去源码中修改,将来我们的类和类之间都有联系,有可能牵一发动全身,所以我们需要将这些数据提取出来,放到文件中,改的时候直接去文件中该,源码不需要改动
1 2 3 4 5 6 7 8 创建配置文件 新建文本文档 xxx.properties 注意事项: 1、key和value都是key=valie形式 2、key和value都是String的,但是不要加双引号 3、每个键值对写完之后,换行写下一个 4、键值对之间最好不要有空格(可以有,但是不建议) 5、键值对中建议不要用中文(读取可能乱码,需要用转换流转码)
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Demo03 { public static void main (String[] args) throws IOException { Properties p = new Properties (); FileInputStream pis = new FileInputStream ("mondeule01\\student.properties" ); p.load(pis); Set<String> set = p.stringPropertyNames(); for (String s : set) { System.out.println(s+"..." +p.getProperty(s)); } } }
IO流工具类-Commons-io 1 IO开发中,代码量很大,代码重复率高。对应遍历目录,拷贝目录中递归调用,程序就变得复杂
添加第三方jar包 1 2 3 4 5 6 7 8 Commons-io工具类属于第三方,需要添加jar包 jar包:本身是一个压缩包,里面转的都是class文件,我们想使用jar包中的工具类,就需要将相应的jar包解压到当前目录下 引入jar包: 1、在当前模块下创建文件夹,取名为lib或者libs 2、将准备好的jar包放到此文件夹下 3、右键,添加到库
使用工具包 IoUtils类 1 2 3 4 IoUtils类 静态方法: IOUtils.copy(InputStream in, OutputStream out) -> 传递字节流,实现文件复制 IOUtils.closeQuietly(任意流对象)悄悄的释放资源,自动处理close()方法抛出的异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Demo04 { public static void main (String[] args) throws IOException { IOUtils.copy(new FileInputStream ("mondule01\\1.txt" ),new FileOutputStream ("mondule01\\11.txt" )); FileWriter fw = null ; try { fw = new FileWriter ("mondule01\\1.txt" ); fw.write("你好" ); } catch (Exception e){ e.printStackTrace(); } finally { if (fw != null ) { IOUtils.closeQuietly(fw); } } } }
FileUtils类 1 2 3 4 5 6 7 8 9 10 11 FileUtils类 静态方法: FileUtiles.copyDirectoryToDirectory(File src, File dest) 传递File类型的目录,进行整个目录的复制,自动进行递归遍历 参数: src:要复制的文件夹路径 dest:要将文件夹粘贴到哪里去 writeStringToFile(File file, String str) -> 写字符串到文本文件中 String readFileToString(File file) 读取文本文件,返回字符串
IO流总结:
写的类名:FileOutputStream、FileWriter、BufferedOutputStream、BufferedWriter、ObjectOutputStream、OutputStreamWriter
写的方法:writer、BufferedWriter的特有方法newLine()、ObjectOutputStream -> writerObject、OutputStreamWriter -> new对象时指定编码
读的类名:FilelnputStream、FileReader、BufferedlnputStream、BufferedReader、ObjectlnputStream、InputStreamReader
读的方法:read()、BufferedReader 特有方法 readLine()、ObjectlnputStream -> readObject()、InputStreamReader new对象的时候指定编码
网络编程 1 2 3 概述:在网络通信协议下,不同计算机上运行的程序,进行数据传输 比如:通信,视频通话,网游,邮件等 只要是计算机之间通过网络进行数据传输,就存在网络编程
软件结构
C/S结构:全称为Client/Server结构,是指客户端和服务端结构。常见程序有QQ
B/S结构:全称为Browser/Server结构,是指浏览器和服务器结构。之间通过浏览器访问
两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序
服务器概念 1 2 概述:安装了服务器软件的计算机 如:tomcat
网络通信协议:两台计算机在做数据交互时要遵守的规则,协议会对数据的格式,速率等进行规定,只要遵守了这个协议,才能完成数据交互
通信三要素 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [IP地址]:计算机的唯一标识,用于两台计算机之间的连接 这东西很熟悉了,也不多说了 IPV4,IPV6 ipconfig、ifconfig ping [协议]: TCP:面向连接协议 需要先确认连接,才能进行数据交互 三次握手: - 第一次握手,客户端向服务器发出连接请求,等待服务器确认 - 第二次握手,服务端向客户端回送一个响应,通知客户端收到了连接请求 - 第三次握手,客户端再次向服务器端发送确认信息,确认连接 好处:数据安全,能给数据的传输提供一个安全的传输环境 坏处:效率低 UDP:面向无连接协议 好处:效率高 坏处:传输的数据不安全,容易丢失数据包 [端口号] 每一个应用程序的唯一标识 用两个字节表示的整数,它的取值范围是0~65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。
TCP协议的四次挥手
1 2 3 4 5 6 7 8 9 四次挥手,客户端和服务端断开连接的时候进行 - 第一次挥手:客户端向服务端提出结束连接,让服务端做最后的准备工作,此时,客户端处于半关闭状态,即表示不再向服务器发送数据了。但是还可以接收数据。 - 第二次挥手:服务器接收到客户端释放连接的请求后,会将最后的数据发送给客户端,并告知上层的应用进程不再接收数据。 - 第三次挥手:服务器发送完数据后,会给客户端发送一个释放连接的报文。那么客户端接收后就知道可以正式释放连接了。 - 第四次挥手:客户端接收到服务器最后的释放连接报文后,要回复一个彻底断开的报文。这样服务器收到后才会彻底释放连接。这里客户端,发送完最后的报文后,会等待2MSL,因为有可能服务器没有收到最后的报文,那么服务器迟迟没收到,就会再次给客户端发送释放连接的报文,此时客户端在等待时间范围内接收到,会重新发送最后的报文,并重新计时。如果等待2MSL后,没有收到,那么彻底断开。
UDP协议编程 1 2 DatagramSocket -> 好比寄快递找的快递公司 DatagramPacket -> 好比快递公司打包
客户端(发送端) 1 2 3 4 5 6 7 8 9 10 11 12 创建DatagramSocket对象 1、空参:端口号从可用的端口号随机一个使用 2、有参:使用指定的端口号 创建DatagramPacket对象,将数据进行打包 1、要发送的数据 -> byte[] 2、指定接收端的IP 3、指定接收端的端口号 发送数据 释放资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class Send { public static void main (String[] args) throws IOException { DatagramSocket socket = new DatagramSocket (); byte [] bytes = "hello,world" .getBytes(); InetAddress ip = InetAddress.getByName("127.0.0.1" ); int port = 6666 ; DatagramPacket dp = new DatagramPacket (bytes, bytes.length, ip, port); socket.send(dp); socket.close(); } }
服务端(接收端) 1 2 3 4 1、创建DatagramSocket对象,指定服务端的端口号 2、接收数据包 3、解析数据包 4、释放资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Send { public static void main (String[] args) throws IOException { DatagramSocket socket = new DatagramSocket (); byte [] bytes = "hello,world" .getBytes(); InetAddress ip = InetAddress.getByName("127.0.0.1" ); int port = 6666 ; DatagramPacket dp = new DatagramPacket (bytes, bytes.length, ip, port); socket.send(dp); socket.close(); } }
先打开服务端(接收端),然后再运行客户端,即可接收到发送的信息
TCP协议编程
客户端 1 2 3 4 1、创建Socket对象,指明服务端的ip以及端口号 2、调用Socket树象中的getOutputStream,获取输出流发送请求 3、调用Socket中的getinputStream,获取输入流读取响应结果 4、关流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Client { public static void main (String[] args) throws Exception{ Socket socket = new Socket ("127.0.0.1" ,6666 ); OutputStream os = socket.getOutputStream(); os.write("我想打视频" .getBytes()); InputStream is = socket.getInputStream(); byte [] bytes = new byte [1024 ]; int len = is.read(bytes); System.out.println(new String (bytes, 0 , len)); is.close(); os.close(); socket.close(); } }
服务端 1 2 3 4 5 1.创建ServerSocket对象,设置端口号 2.调用ServerSocket中的accept方法,等待客户端连接(该方法返回的是连接服务端的socket对象) 3.调用socket中的getInputStream,用于读取请求 4.调用socket中的getOutputStream,用于给客户端写响应 5.关流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class server { public static void main (String[] args) throws Exception{ ServerSocket ss = new ServerSocket (6666 ); Socket s1 = ss.accept(); InputStream is = s1.getInputStream(); byte [] bytes = new byte [1024 ]; int len = is.read(bytes); System.out.println(new String (bytes, 0 , len)); OutputStream os = s1.getOutputStream(); os.write("打个锤子你打" .getBytes()); os.close(); is.close(); s1.close(); ss.close(); } }
练习:文件上传 问题代码:可恶本本分分按照思路敲的代码有bug
服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class server { public static void main (String[] args) throws Exception{ ServerSocket ss = new ServerSocket (6666 ); Socket socket = ss.accept(); InputStream is = socket.getInputStream(); OutputStream outputStream = new FileOutputStream ("D:\\新桌面\\IO\\服务端复制.jpg" ); int len; byte [] bytes = new byte [1024 ]; while ((len = is.read(bytes)) != -1 ){ outputStream.write(bytes, 0 , len); } System.out.println("以下是给客户端的响应结果" ); OutputStream os = socket.getOutputStream(); os.write("上传成功" .getBytes()); os.close(); outputStream.close(); is.close(); socket.close(); ss.close(); } }
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class client { public static void main (String[] args) throws Exception { Socket socket = new Socket ("127.0.0.1" , 6666 ); OutputStream os = socket.getOutputStream(); InputStream inputStream = new FileInputStream ("D:\\新桌面\\IO\\1.jpg" ); int len; byte [] bytes = new byte [1024 ]; while ((len = inputStream.read(bytes)) != -1 ) { os.write(bytes, 0 , len); } System.out.println("以下是服务端的响应结果" ); InputStream is = socket.getInputStream(); byte [] bytes1 = new byte [1024 ]; len = is.read(bytes1); System.out.println(new String (bytes1, 0 , len)); is.close(); inputStream.close(); os.close(); socket.close(); } }
bug 图片因为我已经反复执行过了,所以显示上次的字节,如果是第一次执行,应该是显示0 字节
这个时候就可以推断出,问题出在服务端
解决方法 1 2 3 4 缺少结束标记,给他一个结束标记进行了 // 给服务端写一个结束标记 socket.shutdownOutput();
只需要在客户端上更改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class client { public static void main (String[] args) throws Exception { Socket socket = new Socket ("127.0.0.1" , 6666 ); OutputStream os = socket.getOutputStream(); InputStream inputStream = new FileInputStream ("D:\\新桌面\\IO\\1.jpg" ); int len; byte [] bytes = new byte [1024 ]; while ((len = inputStream.read(bytes)) != -1 ) { os.write(bytes, 0 , len); } socket.shutdownOutput(); System.out.println("以下是服务端的响应结果" ); InputStream is = socket.getInputStream(); byte [] bytes1 = new byte [1024 ]; len = is.read(bytes1); System.out.println(new String (bytes1, 0 , len)); is.close(); inputStream.close(); os.close(); socket.close(); } }
这个时候其实还有一个问题,复制的图片位置固定了,那么下一次上传就会覆盖掉,在服务端用一个随机方法,随机一个名字
1 2 String s = UUID.randomUUID().toString(); // 生成一个十六进制的随机数
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class server { public static void main (String[] args) throws Exception{ ServerSocket ss = new ServerSocket (6666 ); Socket socket = ss.accept(); InputStream is = socket.getInputStream(); String s = UUID.randomUUID().toString(); String name = s + System.currentTimeMillis(); OutputStream outputStream = new FileOutputStream ("D:\\新桌面\\IO\\" +name+".jpg" ); int len; byte [] bytes = new byte [1024 ]; while ((len = is.read(bytes)) != -1 ){ outputStream.write(bytes, 0 , len); } System.out.println("以下是给客户端的响应结果" ); OutputStream os = socket.getOutputStream(); os.write("上传成功" .getBytes()); os.close(); outputStream.close(); is.close(); socket.close(); ss.close(); } }
多线程处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public class serverTest { public static void main (String[] args) throws Exception { ServerSocket ss = new ServerSocket (6666 ); while (true ) { Socket socket = ss.accept(); new Thread (new Runnable () { @Override public void run () { InputStream is = null ; OutputStream outputStream = null ; OutputStream os = null ; try { is = socket.getInputStream(); String s = UUID.randomUUID().toString(); String name = s + System.currentTimeMillis(); outputStream = new FileOutputStream ("D:\\新桌面\\IO\\" + name + ".jpg" ); int len; byte [] bytes = new byte [1024 ]; while ((len = is.read(bytes)) != -1 ) { outputStream.write(bytes, 0 , len); } System.out.println("以下是给客户端的响应结果" ); os = socket.getOutputStream(); os.write("上传成功" .getBytes()); } catch (IOException e) { throw new RuntimeException (e); } finally { if (os != null ) { try { os.close(); } catch (IOException e) { throw new RuntimeException (e); } } if (outputStream != null ) { try { outputStream.close(); } catch (IOException e) { throw new RuntimeException (e); } } if (is != null ) { try { is.close(); } catch (IOException e) { throw new RuntimeException (e); } } if (socket != null ) { try { socket.close(); } catch (IOException e) { throw new RuntimeException (e); } } } } }).start(); } } }
考虑到实际情况,ss对象就不关闭了,保持服务器一直开启,这个close的处理过于恶心,可以封装一个工具类。
思考题,使用线程池
正则表达式 1 2 3 4 5 6 概述:具有特殊规则的字符串 作用:检验 比如:手机号,身份证号,密码,用户名,邮箱等 String中有一个校验正则的方法: boolean mathes(String regex) 校验字符串是否指定的regex的规则
正则这东西省代码,用着简单,写起来抽象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class Test { public static void main (String[] args) { Scanner sc = new Scanner (System.in); String data = sc.next(); boolean res01 = method01(data); boolean res02 = method02(data); System.out.println("res01 = " + res01); System.out.println("res02 = " + res02); } private static boolean method02 (String data) { return data.matches("[1-9][0-9]{4,14}" ); } private static boolean method01 (String data) { if (data.startsWith("0" )) { return false ; } char [] chars = data.toCharArray(); for (char aChar : chars) { if (aChar < '0' || aChar > '9' ) { return false ; } } if (data.length() < 5 || data.length() > 15 ) { return false ; } return true ; } }
具体使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 字符类 1.[abc]:代表a或者b,或者c字符中的一个。 2.[^abc]:代表除a,b,c以外的任何字符。 3.[a-z]:代表a-z的所有小写字符中的一个。 4.[A-Z]:代表A-Z的所有大写字符中的一个 5.[0-9]:代表0-9之间的某一个数字字符。 6.[a-zA-Z0-9]:代表a-z或者A-Z或者0-9之间的任意一个字符。 7.[a-dm-p]:a到d或m到p之间的任意一个字符 逻辑符 1、&&:并且 2、|:或者 预定义字符 1、".":匹配任何字符。(重点)不能加[] 2、"\\d":任何数字[0-9]的简写;(重点) 3、"\\D":任何非数字[^0-9]的简写; 4、"\\s":空白字符:[\t\n\xOB\f\r]的简写 5、"\\S":非空白字符:[^\s]的简写 6、"\\w”:单词字符:[a-zA-Z_0-9]的简写(重点) 7、"\\W”:非单词字符:[^\w] 数量词 1、X? :X出现的数量为0次或1次 2、X* :X出现的数量为0次到多次 任意次 3、X+ :x出现的数量为1次或多次 X>=1次 4、X{n} :x出现的数量为恰好n次 X=n次 5、X{n,} :x出现的数量为至少n次 X>=n次 x{3,} 6、x{n,m} :x出现的数量为n到m次(n和m都是包含的) n=<x<=m 分组括号 (abc) -> 表示abc为一组,出现
String中和正则表达式相关的方法 1 2 3 4 5 boolean matches(String regex) 判断字符串是否匹配给定的正则表达式。 String[] split(String regex) 根据给定正则表达式的匹配拆分此字符串。 String replaceAll(String regex, String rep1acement) 把满足正则表达式的字符串,替换为新的字符
放一个生成正则的网址
https://www.sojson.com/regex/generate
设计模式 1 2 3 4 5 6 7 8 9 10 11 设计模式(Designpattern),是一套被反复使用、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、保证代码可靠性、程序的重用性,稳定性。 1995年,GoF(GangofFour,四人组)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了23种设计模式。<大话设计模式> 总体来说设计模式分为三大类: 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。--> 创建对象 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。--> 对功能进行增强 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、选代子模式、贵任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
模板方法设计模式 1 模板方法(Temp1ateMethod)模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。明确了一部分功能,而另一部分功能不明确。需要延伸到子类中实现
主要思想是一样的写父类里面,不一样的继承到子类中,然后重写方法,使用时,直接子类点父类中的方法
单例模式 1 2 3 4 5 6 目的:单(一个)例(实例,对象) 让一个类只产生一个对象,供外界使用 分类: 1、饿汉式:迅速new对象 2、懒汉式:不着急new对象的
饿汉式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class e { private e () { } private static e e = new e (); public static e getE () { return e; } }
懒汉式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class singleton { private singleton () { } private static singleton e = null ; public static singleton getE () { if (e == null ) { synchronized (singleton.class){ if (e == null ) { e = new singleton (); } } } return e; } }
Lombok使用 1 2 3 4 5 作用:简化javabean开发 使用: 下载插件idea中下载即可 导jar包 设置
@Getter和@Setter 1 2 3 4 5 6 7 8 9 import lombok.Getter;import lombok.Setter;@Getter @Setter public class person { private String name; private int age; }
@ToString 作用:生成toString()方法。
@NoArgsConstructor和@AllArgsConstructor
@NoArgsConstructor:无参数构造方法。
@AllArgsConstructor:满参数构造方法。
@EgualsAndHashCode 生成hashCode()和equals()方法。
@Data 作用:生成get/set,toString,hashcode,equals,无参构造方法
注解只能写在类上。
不包含有参构造
jdk新特性 ==Lambda表达式== 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 面向对象思想是Java的核心编程思想 强调的是对象,然后使用对象中实现好的功能 比如:去北京,强调的是怎么去 jdk8开始,出现了新的思想:函数式编程思想: 强调的是结果,不强调过程 比如:去北京,强调去了还是没去 Lambda表达式 格式: ()->{} 解释: () :参数位 -> :将参数传递到方法体中 {} :重写方法的方法体
lambda表达式在js中很常见啊,在Java中的一个示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Test { public static void main (String[] args) { new Thread (new Runnable () { @Override public void run () { System.out.println("执行了" ); } }).start(); new Thread (()->{ System.out.println("执行了" ); }).start(); new Thread (()-> System.out.println("执行了" )).start(); } }
主要是利用了jvm虚拟机的反推能力,让代码更加简洁、高级,可读性也变差了
使用前提 1 2 3 4 5 必须是函数式接口做方法传递 函数式接口: 有且只有一个抽象方法的接口,用@FuncitionalInterface去检测 比如:Runnable接口,只要一个run抽象方法,就可以使用lambda表达式
省略规则 1 2 3 4 5 6 7 8 9 10 11 12 13 先观察,后改造 1、观察是否是函数式接口做方法参数传递 2、如果是,考虑使用Lambda表达式 3、调用方法,以匿名内部类的形式传递实参 4、从new接口开始到重写方法的方法名结束,选择,删除 5、在重写方法的参数后面,后面大括号前面加上 -> (过于新手了) 省略规则: 1、重写方法的参数结果可以干掉 2、如果重写方法只要一个参数,所在小括号可以干掉 3、如果方法体只有一句话,那么所在的大括号以及分号可以干掉 4、如果方法体中只有一句话并且带return的,那么所在的大括号,分号以及return可以干掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Test02 { public static void main (String[] args) { person p1 = new person ("张三" ,19 ); person p2 = new person ("李四" ,9 ); person p3 = new person ("王五" ,18 ); ArrayList<person> list = new ArrayList <>(); Collections.sort(list, new Comparator <person>() { @Override public int compare (person o1, person o2) { return o1.getAge()- o2.getAge(); } }); Collections.sort(list, (person o1, person o2) -> { return o1.getAge()- o2.getAge(); }); Collections.sort(list, (o1, o2) -> o1.getAge()- o2.getAge()); } }
函数式接口 1 2 3 概述:有且仅有一个抽象方法的接口 检测方法: @FunctionalInterface
介绍四个函数式接口
Supplier 1 2 3 Supplier<T> 它意味着“供给” -> 想要什么,就返回什么 方法: T get()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Demo01 { public static void main (String[] args) { method01(new Supplier <Integer>() { @Override public Integer get () { int [] ints = {4 ,65 ,21 ,84 ,25 }; Arrays.sort(ints); return ints[ints.length-1 ]; } }); method01(() -> { int [] ints = {4 ,65 ,21 ,84 ,25 }; Arrays.sort(ints); return ints[ints.length-1 ]; }); } private static void method01 (Supplier<Integer> supplier) { Integer i = supplier.get(); System.out.println("i = " + i); } }
Consumer 1 2 3 Consumer<T> -> 消费性接口 -> 操作 方法: void accept(T t) 意为消费一个指定泛型的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Demo02 { public static void main (String[] args) { method(new Consumer <String>() { @Override public void accept (String s) { System.out.println(s.length()); } },"asdfghjkl" ); method(s-> System.out.println(s.length()),"asdfghjkl" ); } private static void method (Consumer<String> consumer, String s) { consumer.accept(s); } }
Function 1 2 3 Function<T,R> 接口用来根据一个类型数据得到另一个类型数据 方法: R apply(T t) 根据类型T参数获取类型R的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Demo03 { public static void main (String[] args) { method(new Function <Integer, String>() { @Override public String apply (Integer integer) { return "" ; } },100 ); method(integer -> integer+"" ,200 ); } private static void method (Function<Integer, String> function, Integer number) { String s = function.apply(number); System.out.println(s+1 ); } }
Predicate 1 2 判断型接口 Predicate<T> boolean test(T t) -> 用于判断方法,返回布尔型
1 2 3 4 5 6 7 8 9 10 public class Demo04 { public static void main (String[] args) { method(s -> s.length()==7 ,"qwertyu" ); } private static void method (Predicate<String> predicate, String s) { boolean test = predicate.test(s); System.out.println("test = " + test); } }
但是这个四个函数式接口,毫无用处,绕了一圈使用Lambda表达式解决问题罢了。但其实,这四个函数式接口主要配合Stream流使用的
==Stream流== 1 流不是指IO流,是一种编程方式--“流式编程”,可以看作流水线
演示一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class Demobig { public static void main (String[] args) { ArrayList<String> list =new ArrayList <>(); list.add("张三" ); list.add("张三丰" ); list.add("翠花" ); list.add("翠花" ); list.add("赵正" ); list.add("张无忌" ); list.add("潘金莲" ); list.add("武大郎" ); list.add("张辽" ); list.add("张八百" ); ArrayList<String> list1Zhang = new ArrayList <>(); for (String s : list) { if (s.startsWith("张" )) { list1Zhang.add(s); } } ArrayList<String> list1 = new ArrayList <>(); for (String s : list1Zhang) { if (s.length() == 3 ) { list1.add(s); } } for (String s : list1) { System.out.println(s); } System.out.println("使用Stream流" ); Stream<String> stream = list.stream(); System.out.println("使用Lambda表达式" ); stream.filter(s -> s.startsWith("张" )).filter(s -> s.length()==3 ).forEach(s -> System.out.println(s)); } }
Stream流的获取 1 2 3 4 5 针对集合:Collection中的方法 Stream<E> stream() 针对集合:Stream接口中的静态方法 static <T> Stream<T> of(T...values)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class steam { public static void main (String[] args) { ArrayList<String> list = new ArrayList <>(); list.add("张三" ); list.add("李四" ); list.add("王五" ); list.add("张三" ); Stream<String> stream = list.stream(); System.out.println(stream); Stream<String> stream1 = Stream.of("张三" ,"李四" ,"王五" ); System.out.println(stream1); } }
Stream方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 forEach:逐一处理 -> 遍历 void forEach(Consumer<? super T> action); 注意:forEach方法是一个[终结方法],使用完之后,Stream流不能用了 count:统计元素个数 long count(); 注意:count方法也是终结方法 filter:根据某个条件进行元素过滤 Stream<T> filter(Predicate<? super T> predicate) 返回一个新的stream流对象 limit:获取前n个元素 Stream<T> 1imit(long maxSize) 获取Stream流对象中的前n个元素,返回一个新的stream流对象 skip:跳过Stream流中的前n个元素 Stream<T> skip(long n) 跳过stream流对象中的前n个元素,返回一个新的stream流对象 concat:流合并 static <T> Stream<T> concat(Stream<? extends T> a, Stream<?extends T>b) 两个流合成一个流 collect:Stream流转换成集合 dinstinct:元素去重,依赖hashCode和equals方法 如果是自定义对象类型,需要重写两个方法 map:转化流中的数据类型 Stream<R> map(Function<T,R> mapper)
小练习
1 2 3 4 5 6 7 8 9 10 11 1.第一个队伍只要名字为3个字的成员姓名 2.第一个队伍筛选之后只要前3个人; 3.第二个队伍只要姓张的成员姓名; 4.第二个队伍筛选之后不要前2个人; 5.将两个队伍合并为一个队伍; 6.打印整个队伍的姓名信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Test { public static void main (String[] args) { ArrayList<String> one = new ArrayList <>(); one.add("迪丽热巴" ); one.add("宋远桥" ); one.add("苏星河" ); one.add("老子" ); one.add("庄子" ); one.add("孙子" ); one.add("洪七公" ); ArrayList<String> two = new ArrayList <>(); two.add("古力娜扎" ); two.add("张无忌" ); two.add("张三丰" ); two.add("赵丽颖" ); two.add("张二狗" ); two.add("张天爱" ); two.add("张三" ); Stream<String> stream = one.stream(); Stream<String> s1 = stream.filter(s -> s.length()==3 ).limit(3 ); Stream<String> stream1 = two.stream(); Stream<String> s2 = stream1.filter(s -> s.startsWith("张" )).skip(2 ); Stream.concat(s1,s2).forEach(s -> System.out.println(s)); } }
代码还可以更恶心
1 2 3 4 Stream<String> stream = one.stream(); Stream<String> stream1 = two.stream(); Stream.concat(stream.filter(s -> s.length()==3).limit(3),stream1.filter(s -> s.startsWith("张")).skip(2)).forEach(s -> System.out.println(s));
方法引用 1 2 3 4 5 6 概述:引用方法 使用: 1、被引用的方法要写在重写方法内 2、被引用的方法从参数上,返回值上要和所在重写方法一致,而且引用的方法最好是操作重写方法的参数值的 3、干掉重写方法的参数: 干掉->、干掉被引用方法的参数、被引用方法的.改成::
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Demo01 { public static void main (String[] args) { Stream<String> stream = Stream.of("三上" ,"八下" ,"张三" ); System.out.println("Lambda表达式" ); System.out.println("方法引用" ); stream.forEach(System.out::println); } }
对象名引用成员方法 1 2 3 4 5 6 7 8 格式: 对象::成员方法名 需求: Supplier<T> 接口 方法: T get()
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Demo02 { public static void main (String[] args) { method(()-> " abc " .trim()); method(" abc " ::trim); } private static void method (Supplier<String> supplier) { String s = supplier.get(); System.out.println("s = " + s); } }
类名引用静态方法
1 2 3 4 5 6 7 8 9 10 11 public class Demo03 { public static void main (String[] args) { method(()->Math.random()); method(Math::random); } private static void method (Supplier<Double> supplier) { Double aDouble = supplier.get(); System.out.println(aDouble); } }
类-构造引用 1 2 3 4 5 格式: 构造方法名::new Function<T, R>接口 R apply(T t) 用于数据类型转换
1 2 3 4 5 6 7 8 9 10 11 public class Demo04 { public static void main (String[] args) { method(s -> new person (s),"李四" ); method(person::new ,"李四" ); } private static void method (Function<String,person> function, String name ) { person person = function.apply(name); System.out.println("person = " + person); } }
数组引用 1 2 3 格式: 数组的数据类型[]::new int[]::new -> 创建一个int型数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Demo05 { public static void main (String[] args) { method(new Function <Integer, int []>() { @Override public int [] apply(Integer integer) { return new int [integer]; } },10 ); method(integer -> new int [integer],10 ); method(int []::new ,10 ); } private static void method (Function<Integer, int []> function, Integer len) { int [] arr = function.apply(len); System.out.println("arr = " + arr); } }
jdk10更新了局部变量类型推断,可以想前端一样写
文本块 jdk15
三引号“”“ ”“” 自动给文本保持格式,不需要手动的/n
jdk16 instanceof匹配
instanceof这个关键字是强转时使用的
1 2 3 4 5 6 7 8 9 10 11 12 public static void method(Animal animal) { /*if (animal instanceof Dog){ Dog dog =(Dog) animal; dog.eat(); dog.1ookDoor; }*/ if (animal instanceof Dog dog) { dog.eat(); dog.lookDoor(); } }
只需要多给一个变量名,就省略了强转的过程
Record类
是一种全新的类型,本质上是一个final类,同时所有的类型都是final修饰,它会自动编译出get,set,hashCode,toString,以及比较所有属性值的equals。
注意:
Record只会有一个全参构造
重写的equals方法比较所以的属性值
可以在Record声明的类中定义静态字段、静态方法或实例方法(非静态成员方法)
不能在Record声明的类中定义实例字段(非静态成员变量)
类不能声明为abstract
不能显式的声明父类,默认父类是java.lang.Record类
因为Record是final类,所以没有子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public record Test (String name) { static int i; public static void method () { }; public void method01 () { }; }
密封类
密封的类和接口限制其他可能继承或实现它们的其他类或接口。
1 2 3 4 【修饰符】 sealed class 密封类 【extends父类】 【implements父接口】 permits 子类{ } 【修饰符】 sealed interface 接口 【extends父接口们】 permits 实现类{ }
密封类使用sealed修饰符
使用permits关键字来指定可以继承或实现该类的类型
一个类继承密封类或实现密封接口,该类必须是sealed、non-sealed、final修饰的
sealed修饰的类或接口必须有子类或实现类
Junit单元测试 1 2 3 Junit是单元测试框架,可以替代main方法去执行其他方法 作用:可以单独执行一个方法,测试该方法是否能抛通 注意:Junit是第三方工具
jar包下载地址
https://github.com/junit-team/junit4/wiki/Download-and-Install
配置下载地址
https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/
JUnit的注意事项 1 2 3 4 5 1、@Test不能修饰static方法 2、@Test不能修饰带参数的方法 3、@Test不能修饰带返回值的方法 这个很好理解测试方法,只测试一个方法,带参数,带返回值就需要进行传参和返回了,就超出了这个方法的范围
相关注解 1 2 3 4 5 6 7 8 @Before:在@Test之前执行,有多少个@Test执行,@Before就执行多少次 -> 都是用作初始化一些数据 @After:在@Test之前执行,有多少个@Test执行,@After就执行多少次 -> 用作释放资源 还有两个注解 @Beforecass:在@Test之前执行,只执行一次,只能修饰静态方法 @Afterclass:在@Test之后执行,只执行一次,只能修饰静态方法
@Test主要是用来测试的,用来代替main方法,分来测试模块的功能。
类的加载机制 1 2 3 4 5 1、new对象 2、new子类对象(new子类对象先初始化父类) 3、执行main方法 4、调用静态成员 5、反射,创建class对象
类加载器(了解)ClassLoader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 1.概述: 在jvm中,负责将本地上的c1ass文件加载到内存的对象ClassLoader 2.分类: BootstrapclasLoader:根类加教器 -> C语言写的,我们是获取不到的 也称之为引导类加载器,负责Java的核心类加载的 比如:System,String等 jre/lib/rt.jar下的类都是核心类 ExtclassLoader:扩展类加载器 负责jre的扩展目录中的jar包的加载 在jdk中jre的lib目录下的ext目录 AppclassLoader:系统类加载器 负责在jvm启动时加载来自java命令的c1ass文件(自定义类),以及classPath环境变量所指定的jar包(第三方jar包) 不同的类加载器负责加载不同的类 3.三者的关系(从类加载机制层面): Appc1assLoader的父类加载器是ExtClassLoader ExtclassLoader的父类加载器是BootStrapclassLoader 但是:他们从代码级别上来看,没有子父类继承关系->他们都有一个共同的父类->ClassLoader 4.获取类加载器对象:getclassLoaderQ是class对象中的方法 类名.class.getclassLoader() 5.获取类加教器对象对应的父类加教器 ClassLoader类中的方法:classLoader getParent()-> 没啥用 6.双亲委派(全盘负责委托机制) a.Person类中有一个string Person本身是AppclassLoader加载 String是BootStrapclassLoader加较 b.加载顺序: Person本身是App加载,按道理来说string也是App加载 但是App加载String的时候,先问一问Ext,说:Ext你加载这个string吗? Ext说:我不加载,我负贵加载的是扩展类,但是app你别着急,我问问我爹去->boot Ext说:boot,你加载string吗? boot说:正好我加载核心类,行吧,我加载吧! 7.类加载器的cache(缓存)机制(扩展):一个类加载到内存之后,缓存中也会保存一份儿,后面如果再使用此类,如果缓存中保存了这个类,就直接返回他,如果没有才加载这个类,下一次如果有其他类在使用的时候就不会重新加载了,直接去缓存中拿,保证了类在内存中的唯一性 8.所以:类加载器的双亲委派和缓存机制共同造就了加载类的特点:保证了类在内存中的唯一性
缓存内有就开始加载,如果没有向上询问,直到boot,如果也没有去规定的目录内寻找,如果没有再向下查找
反射 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1.反射概述:解剖c1ass对象的一个技术 2.问题:能解剖c1ass对象的啥呢? a.解剖出成员变量:赋值 b.解剖出成员方法:调用 c.解剖出构造方法:new对象 3.用反射的好处:让代码变的更通用,更灵活 4.怎么学反射: a.将反射看成是一套API来学 b.通过案例,体会反射的好处 5.问题:玩儿反射,最开始的一步是干啥? 获取class对象 6.c1ass对象:c1ass文件对应的对象 c1ass类:描述class对象的类
获取class对象 1 2 3 4 5 6 7 8 9 10 方式一:调用Object中的getClass方法: class<?> getClass() 方式二:不管是基本类型还是引用类型,jvm都为其提供了一个静态成员:c1ass 方式三:C1ass类中的静态方法: static Class<?> forName(String className) className:传递的是类的全限定名(说的很高级,其实就是 包名.类名 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Test { @org .junit.Test public void add () throws ClassNotFoundException { person person = new person (); Class<? extends person > class1 = person.getClass(); System.out.println(class1); Class<person> class2 = person.class; System.out.println(class2); Class<?> class3 = Class.forName("com.sjjws.h_001.person" ); System.out.println(class3); } }
forName是最通用的,因为参数是String,可以和properties文件,通过IO流的一番操作,就可以获取到class对象
类名.class是最常用的,因为方便
获取构造方法 获取所有public的构造方法 1 2 Class类中的方法: Constructor<?>[] getConstructors() -> 获取所有public的构造
1 2 3 4 5 6 7 8 public class Demo { public static void main (String[] args) { Class<person> aClass = person.class; for (Constructor<?> constructor : aClass.getConstructors()) { System.out.println("constructor = " + constructor); } } }
获取空参构造 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 获取: Class类中的方法: Constructor<T> getConstructor(Class<?>... parameterTypes) -> 获取指定的public的构造 parameterTypes:可变参数,可以传递0-多个参数 1、如果获取空参构造,参数不用写 2、如果获取有参构造:参数写参数类型的class对象 调用: Constructor类中的方法: T newInstance(Object...initargs) -> 创建对象 initargs:传递的是构造方法的实参 a.如果根据无参构造new对象,initargs不写了 b.如果根据有参构造new对象,initargs传递实参
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Demo02 { public static void main (String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Class<person> aClass = person.class; Constructor<person> constructor = aClass.getConstructor(); System.out.println("constructor = " + constructor); person person = constructor.newInstance(); System.out.println("person = " + person); } }
上面的代码是先获取空参构造的方法,然后再用这个方法来new对象
利用空参new对象,有更简单的方法
1 2 3 4 5 6 Class类中的方法: T newInstance() -> 根据空参构造创建对象 前提:被反射的类中必须有public修饰的空参构造。 注意:这个方法以及过时了
获取有参构造 和无参构造相差无几
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Demo03 { public static void main (String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Class<person> aClass = person.class; Constructor<person> constructor = aClass.getConstructor(String.class, int .class); System.out.println("constructor = " + constructor); person person = constructor.newInstance("张三" ,19 ); System.out.println("person = " + person); } }
获取私有构造 1 2 3 4 5 6 7 8 9 10 constructor<?>[] getDeclaredconstructors() -> 获取所有构造方法,包括private onstructor<T> getDeclaredconstructor(类<?>...parameterTypes) -> 获取指定构造,包括private parameterTypes:参数类型的class对象 获取到的私有构造方法是不能直接使用的,但是反射是流氓啊 Constructor的父类AccessibleObject内有一个方法 void setAccessible(boolean flag) -> 修改访问权限 flag为true:解除私有权限
1 2 3 4 5 6 7 8 9 public class Demo04 { public static void main (String[] args) throws Exception{ Class<person> aClass = person.class; Constructor<person> constructor = aClass.getDeclaredConstructor(String.class); constructor.setAccessible(true ); person person = constructor.newInstance("张三" ); } }
反射成员方法 获取所有public的成员方法 1 2 Class类中的方法: Method[] getMethods() -> 获取所有public方法,包括父类中的public方法
1 2 3 4 5 6 7 8 9 10 public class Demo05 { public static void main (String[] args) { Class<person> aClass = person.class; Method[] methods = aClass.getMethods(); for (Method method : methods) { System.out.println(method); } } }
获取public方法(有参、无参) 1 2 3 4 5 6 7 8 9 10 11 12 Class类中的方法: Method getMethod(String name,Class<?>...parameterTypes) -> 获取指定的public的成员方法 name:传递方法名 parameterTypes:方法参数类型的class对象(因为是存在重载方法这个东西的) 调用方法: Method对象中的方法: Object invoke(Object obj, Object... arges) -> 执行方法 obj:根据构造new出来的对象 arges:方法实参 -> 如果有参数直接传 返回值:Object -> 接收被执行方法的返回值,如果方法没有返回值,不接受
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Demo06 { public static void main (String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { Class<person> aClass = person.class; person person = aClass.newInstance(); Method method = aClass.getMethod("setName" , String.class); method.invoke(person, "张三" ); System.out.println(person); Method get = aClass.getMethod("getName" ); Object o = get.invoke(person); System.out.println(o); } }
获取私有方法 1 2 3 4 5 6 7 8 Method[] getDeclaredMethods() -> 获取所有的成员方法,包括private的 Method getDeclaredMethod(String name, class<?>... parameterTypes) -> 获取执行成员方法,包括private name:传递方法名 parameterTypes:方法参数类型的class对象 解除私有权限:void setAccessible(boolean flag)
1 2 3 4 5 6 7 8 9 10 public class Demo07 { public static void main (String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { Class<person> aClass = person.class; person person = aClass.newInstance(); Method method = aClass.getDeclaredMethod("eat" ); method.setAccessible(true ); method.invoke(person); } }
反射成员变量 获取所有属性 1 2 3 4 Class类中的方法: Field[] getFileds() -> 获取所有public的属性 Field[] getDeclaredFields() -> 获取所有属性,包括priavte的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Demo08 { public static void main (String[] args) { Class<person> aClass = person.class; Field[] fields = aClass.getFields(); for (Field field : fields) { System.out.println("field = " + field); } Field[] fields1 = aClass.getDeclaredFields(); for (Field field : fields1) { System.out.println("field = " + field); } } }
获取指定属性 1 2 3 4 5 6 7 8 9 10 11 12 Class类中的方法: Field getFiled(String name) -> 获取指定public的属性 Field getDeclaredField(String name) -> 获取指定属性,包括priavte的 Field类中的方法: void set(Object obj, Object value) -> 为属性赋值,相当于JavaBean中的set方法 obj:对象 value:赋予的值 Object get(Object obj) -> 获取属性值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Demo08 { public static void main (String[] args) throws NoSuchFieldException, InstantiationException, IllegalAccessException { Class<person> aClass = person.class; person person = aClass.newInstance(); Field fields = aClass.getField("name" ); fields.set(person, "张三" ); Object o = fields.get(person); System.out.println("o = " + o); } } public class Demo09 { public static void main (String[] args) throws NoSuchFieldException, InstantiationException, IllegalAccessException { Class<person> aClass = person.class; person person = aClass.newInstance(); Field fields = aClass.getDeclaredField("age" ); fields.setAccessible(true ); fields.set(person, 19 ); Object o = fields.get(person); System.out.println("o = " + o); } }
反射练习 1 2 3 4 5 可以看到反射操作class对象的方法都不是很简单(相比直接new对象来说),反射的主要作用是编写框架 public interface 接口名{ public Employee find() }
1 2 3 4 5 <select id="find" resultType="Employee的全限定名"> select 列名 from 表名 where 条件 </select> 根据接口的class对象,创建一个实现类对象,然后通过配置文件中的方法名反射这个方法,invoke执行这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 练习需求:在配置文件中。配置类的全限定名,以及配置一个方法名,通过解析配置文件,让配置好的方法执行起来 步骤: 1.创建properties配置文件,配置信息 问题一:配置文件放在那里 开发完给用户的是out路径下的class文件,将class文件打包,如果配置文件在模块下,out路径下是不会生成这个配置文件的 解决:将配置文件放到src目录下 问题二:将配置文件放到src下,out路径下会自动生成配置文件,但是如果我们将来将所有的配置文件都放到src下,那么src下面会显得特别乱 解决:我们可以单独创建一个文件夹,将所有的配置文件放到此文件夹下,将此文件夹改成资源目录,取名为resources 2.读取配置文件,解析配置文件 问题一:如果将配置文件放到resources资源目录下,我们怎么读取 new FileInputstream("模块名\\resources\\properties文件名") -> 这样不行,因为out路径下没有resources->相当于写死了 问题解决:用类加载器 classLoader classLoader = 当前类.class.getclassLoader() Inputstream in = classLoader.getResourceAsStream("文件名")//自动扫描resources下的文件->可以简单理解为扫描out路径下的配置文件 3.根据解析出来的c1assName,创建c1ass对象 4.根据解析出来的methodName,获取对应的方法 5.执行方法
配置文件
1 2 className=com.sjjws.aa_kj.Person methodName=eat
定义方法
1 2 3 4 5 public class Person { public void eat () { System.out.println("狠狠干饭" ); } }
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class Demo { public static void main (String[] args) throws Exception{ Properties properties = new Properties (); InputStream in = Demo.class.getClassLoader().getResourceAsStream("student.properties" ); properties.load(in); String className = properties.getProperty("className" ); String methodName = properties.getProperty("methodName" ); Class<?> aClass = Class.forName(className); Object o = aClass.newInstance(); Method method = aClass.getMethod(methodName); method.invoke(o); } }
这种方法看似很麻烦,但是体现了框架架构思想
注解 1 2 3 4 5 6 7 引用数据类型: 类 数组 接口 枚举 注解 作用: 说明:对代码进行说明,生成doc文档(API文档) 检查:老朋友 @Override 检查是否为重写方法、 @FunctionalInterface 检查是否为函数式接口 分析:对代码进行分析,起到配置文件的作用
1 2 3 4 JDK中的注解: @override -> 检测此方法是否为重写方法 @Deprecated -> 提示方法已经过时,不推荐使用调用方法的时候,方法上会有横线,但是能用 @suppresswarnings -> 消除警告@Suppresswarnings("a11")
注解的定义以及属性的定义格式 注意,这里说的注解属性,其实本质上是抽象方法,但是我们按照属性来理解,好理解,因为到时候使用注解的时候,需要用=为其赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 定义: public @interface 接口名{ } 定义属性(抽象方法): 数据类型 属性名() -> 在使用注解的时候为其赋值 数据类型 属性名() default 值 -> 有默认值,如果需要可以二次赋值 注解可以定义的类型: 1、8种基本数据类型 2、String类型、class类型、枚举类型、注解类型 3、以及以上类型的一维数组
1 2 3 4 5 6 7 8 9 public @interface Book { String bookName () ; String[] author(); int price () ; int count () default 10 ; }
注解的使用 1 2 3 4 5 6 7 8 9 10 使用: 本质上就是为注解中的属性赋值 使用位置: 在类上使用、在方法上使用、成员变量上使用、局部变量上使用、参数位置使用等 使用格式: 1、@注解名(属性名 = 值...) 2、如果属性中有数组: @注解名(属性名 = {元素1,元素2...})
注意事项:
1、空注解可以直接使用 -> 空注解就是注解中没有任何的属性
2、不同的位置可以使用一样的注解,同一位置不可以
3、使用注解时,如果此注解中有属性,注解中的属性一定要赋值,如果有多个属性,用,隔开,如果有数组使用{}
4、如果注解中的属性有默认值,那么我们不必要写,也不要重新赋值,反之必须写
5、如果注解中只有一个属性,并且属性名为value,那么使用的时候可以不写属性名,只写值
注解解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 概述:获取注解中的属性值 AnnotatedElement接口 实现类:Accessibleobject,Class,Constructor,Executable,Field,Method,Package,Parameter (可以看出这些实现类和反射密切相关) 先判断位置上有没有使用指定的注解,如果有,获取指定的注解,获取注解中的属性值 boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) -> 判断指定位置上有没有指定的注解 比如:判断BookShe1f上有没有Book注解 Class bookshelf = Bookshelf.class bookShelf.isAnnotationPresent(Book.class) getAnnotation(Class<T> annotationClass) -> 获取指定是注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Test { public static void main (String[] args) { Class<BookShelf> bookShelfClass = BookShelf.class; boolean b = bookShelfClass.isAnnotationPresent(Book.class); System.out.println(b); if (b){ Book book = bookShelfClass.getAnnotation(Book.class); System.out.println(book.bookName()); System.out.println(Arrays.toString(book.author())); System.out.println(book.price()); System.out.println(book.count()); } } }
但是这个代码并没有获取到,输出 b 输出结果为false
原因是Book注解没有被加载到内存
元注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 概述:元注解就是管理注解的注解 管理: 1、控制注解的使用位置 控制注解是否能在类上使用 控制注解是否能在方法上使用 控制注解是否能在构造上使用等 2、控制注解的生命周期(加载位置) 控制注解是否能在源码中出现 控制注解能否在class文件中出现 控制注解能否在内存中出现 3、怎么使用: a.@Target:控制注解的使用位置 属性:ElementType[] value() ElementType是一个枚举,里面的成员可以类名直接调用 ElementType中的成员: TYPE:控制注解能使用在类上 FIELD:控制注解能使用在属性上 METHOD:控制注解能使用在方法上 PARAMETER:控制注解能使用在参数上 CONSTRUCTOR:控制注解能使用在构造上 LOCAL_VARIABLE:控制注解能使用在局部变量上 不写@Target的情况下哪都能写,如果写了@Target那么就只能按照控制的位置写 b.@Retention:控制注解的生命周期(加载位置) 属性:RetentionPolicy value(); RetentionPolicy也是枚举 RetentionPolicy中的成员: SOURCE:控制注解能在源码中出现->默认 CLASS:控制注解能在c1ass文件中出现 RUNTIME:控制注解能在内存中出现
将Book加载到内存,再次运行就出现了
1 2 3 4 5 6 7 8 9 10 @Retention(RetentionPolicy.RUNTIME) public @interface Book { String bookName () ; String[] author(); int price () ; int count () default 10 ; }
枚举 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 1、概述:五大引用数据类型 类 数组 接口 注解 枚举 2、定义: public enum 枚举类名{ } 所有的枚举类都有一个共同的父类Enum 3、定义枚举值: 特点: 都是static final,但是定义的时候不要写出来,写出来会报错 写完所有的枚举值之后,最后加个; 枚举值的名字大写 -> 开发习惯 使用:类名直接调用 注意:每一个枚举值都是当前枚举类的对象 4、问题:枚举类中的枚举值都是什么类型? 本类型 5、枚举类中的其他成员:构造 在枚举类中定义的构造,默认都是private的 6、枚举的使用场景: 表示对象的状态
枚举还可以设置值,设置了值的枚举值,相当于进行了一个有参构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public enum state { WEIFUKUAN("未付款" ), YIFUKUAN, WEIFAHUO, YIFAHUO; private String name; state(String name){ this .name = name; } state(){} public String getName () { return name; } }
在后面传入参数,还方便调用
1 2 3 4 5 6 7 8 public class Demo02 { public static void main (String[] args) { state state0 = com.sjjws.aa_kj.state.WEIFAHUO; System.out.println(state0); System.out.println(state0.getName()); } }
枚举的方法
方法名
说明
String toString()
返回枚举值的名字
values()
返回所有与的枚举值
valueof(String str)
将一个字符串转成枚举类型
注意,valueOf内的字符串必须和定义好的枚举值一模一样,包括大小写,感觉很多此一举啊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Demo02 { public static void main (String[] args) { String s = state.WEIFUKUAN.toString(); System.out.println("s = " + s); state[] states = state.values(); for (state state : states) { System.out.println(state); } state s2 = state.valueOf("YIFAHUO" ); System.out.println(s2); } }