由于之前是使用word编写,所以可能效果不如人意,可以私信找我拿资源
由于之前学习JavaSE之前自学了JavaScript,所以有部分理解是基于JavaScript
比较基础的DOS命令
按住win+R 输入cmd,打开DOS窗口
exit 退出dos窗口;
cls 清楚dos窗口记录;
dir 列出当前目录下的所有子文件/子目录;
cd 改变目录,cd 目录的路径,cd..回到上级目录,cd\直接回到根目录c:回车切换到c盘
进制
使用计算机自带的计算器,win+r 输入calc就可以打开计算器了,然后选择程序员标准的计算器
1byte(字节) = 8bit(位) 注意大写的B代表字节,小写的b代表位
一、标识符
自己定义的东西的名字就是标识符(类名,变量名,方法名)
1、开头区分字母大小写,数字,下划线_,美元符$
2、不能以数字开头,不能是关键字
3、区分大小写main
命名规范:驼峰式
包:全部小写,以域名开头
类(接口)名字:大驼峰 FirstJavaDome
函数和变量名:小驼峰 firstJavaDome
二、常量与变量
使用方法和var差不多,并用分号结束
字面值常量:
字符串、整数、小数、字符、布尔、空常量
自定义常量:
在变量前面加上一个 关键字:final
final int x = 90;
这就定义了一个常量,常量是不能够修改的,修改会发生报错等
注意事项:定义float和long时,要注意后面要加上F(f)和L(l);
变量类型:
整数类型:byte short int long
浮点类型:float double
字符类型:char
整数默认为int,小数默认为double,有时候在编译的时候会发生错误,所以最好在long和float类型的数值后加上一个类型字母首字母。
布尔类型:就true和false两个。
注意事项:使用之前一定要初始化(定义一个值);作用域;一次创建多个变量是和js一样,int x, y, z; 创建多个变量;而且char也可以储存汉字
局部变量
语句块里的变量,存活周期只能是从语句块的开始到语句块的结束(方法)
成员变量
方法的外部,类的内部定义的变量。从属于对象,生命周期伴随着对象始终,如果不手动初始化,则会自动初始化。
变量类型 | 自动初始化值 |
Int | 0 |
Double | 0.0 |
Char | ‘\u0000’ |
Boolean | False |
静态变量
从属于类,用static定义,周期最长。
三、运算符
都可以对整数和浮点进行运算,运算过程中只要都是整数类型,则算出来的值也只能是整数类型,只要有浮点类型就会有浮点类型。
算术运算符
+、_、*、/、% 与JavaScript一样,连字符串啥的规则也都适合JavaScript也一样,但是字符不一样,字符在java里面代表的是一个整数,然后将字符代表的整数与数字进行计算
但是用浮点进行运算的时候会有点不精确,因为float和double这两个类型在java里本身就不精确。
自增自减运算符
++ -- 还是与JavaScript里面的一样,放在前面后面也都有区别。
赋值运算符
=、+=、*=、/=、%= 和JavaScript一样
比较运算符
<、<=、>、>=、==、!= 和JavaScript一样,并且要注意==和=,=是赋值运算符,==是比较运算符。
逻辑运算符
两边的操作数必须是布尔类型的
如何比较:boolean 名字 = 规则1 运算符 规则二;(也可以直接进行比较,不需要接收)
&& 与 都真才是真,有一假就是假
|| 或 有一真就是真,都假才是假
^ 异或 一边真一边假才是true,都一样就是flase
! 取反(非) 放在比较前面 !(a>b),然后取反
&和| 不管返回答案如何,他都会把整条语句执行完,但是&&和||不会,只要知道返回是false或者true的时候,后面就不会执行
位运算符(只针对二进制,不是很重要)
计算机是二进制,所有的数据使用二进制来储存。对于每个二进制来说都有原码,反码,补码。计算机存储数据的时候,使用第一位表示数字的正负,0代表正数,1代表负数。计算机存储正数的时候直接存储正数的原码,存储负数的时候存储负数的补码。
正数:原码、反码、补码相同。
负数:
原码 符号位1
反码 符号位不变,其余位数取反
补码 反码+1
只针对计算机里面存储的二进制(补码)进行运算(符号位也参与运算)
& 按位与:对应位都是1才是1,然后其他的就是0,组成一个补码
| 对应位有一个是1,就是1
~ 放在数字前面和非很像,按位取反,1变0,0变1
^ 按位异或,对应位不一样才是1
<< 高位抛弃,低位补0
>> 低位抛弃,高位按照符号位补齐
>>> 忽略符号位,低位抛弃,高位补0
三元运算符
?:
使用规则:布尔值?表达式1:表达式2
要是布尔值返回的是true则返回表达式1,布尔值返回的是false则返回表达式2。
运算符优先级
不需要去记优先级,通过括号去控制优先级,有时候默认的优先级我们是区分不了的,最好的方法就是通过括号去控制优先级
四、数据类型转换
隐式类型转换
在赋值语句和算术表达式中,系统会自动将类型实现转换。
字符会自动转换成整数类型,按照ASC码来转换
强制类型转换
数据1 = (强制转换类型)数据2; 如:
byte a = 10;
double f = 100;
a = (byte)f;
System.out.println(a);
注意:浮点类型强制转换成整数类型的时候,会自动把小数部分给略去。
注意事项
强制类型转换一般不推荐使用,因为会有损失精度和数据溢出的可能
byte/short/char可以进行更具ASCII码表进行数学运算,在进行数学运算的时候byte/short/char首先被提升成int类型,例如“+“
byte+byte àint + int àint
byte+short àint + int àint
boolean类型不能发生转换,false就是false,true就是true
五、语句控制
语句块
以{开始,以}结束。
遇到单纯语句块的时候,会直接执行语句块里面的内容,从上到下,并且语句块里面可以访问到外面的变量,但是外面访问不了里面的变量。
流程控制
控制语句中执行的顺序,默认是从上到下。
条件语句
if (布尔表达式) {
}
if (condition) {
} else {
}
if (condition) {
} else if{
}else{
}
只有布尔表达式返回为true的时候,才会执行,具体和JavaScript一样。
当if语句后面没有大括号的时候,系统会自动默认if后的第一条语句为语句块。
三目运算符可以被if语句完全代替,但是在某些时候三目运算符更简洁。
switch语句
和JavaScript大致一样,也可以用break来中断判断
switch (a) {
case 1:
System.out.println("a的值是1");
break;
default:
break;
}
将case后面的值和表达式(switch后面括号里的值)作比较,要是一样则执行case后:的语句,要是表达式和上面所有的case语句都不匹配的话则会返回default语句里的内容,表达式可以是整数,字符,字符串,枚举;case后的值要是常量,不能是变量。
循环语句
while循环,和JavaScript一样
int i = 1;
while (i <= 10) {
System.out.println("ljy");
i++;
}
do{}while();循环
do {
循环体
} while (条件判断);
也是先执行一次循环体在判断在执行;
for循环,和JavaScript一样
for (int a = 0; a < 10; a++) {
System.out.println(a);
}
continue和return
continue 跳出本次循环,执行下一次循环
return 结束方法,整个方法都结束;返回值。和JavaScript一样。
方法
可以被我们反复的调用,有点类似于JavaScript的function。
返回值类型:事先约定好返回值类型,如无返回值,则必须显示指定为viod。
public class App {
public static void main(String[] args) throws Exception {
App ff = new App();
ff.abc(20, 30, 40) ;
}
// 方法
void abc(int a, int b, int c) {
int d = a + b + c;
System.out.println(d);
}
public static void name() {
System.out.println();
}
}
要是用return返回一个数字类型的值回来,则只需要把void改成int。(调用之前注意要先new一下,但是在 void前加上public static就可以不需要new)。实参的类型和类一定要与形参匹配,return的返回值一定要与返回值类型相对应。带static就是一个普通方法
方法重载
独立的完全不同的方法,只是传参的个数不一样而已,所以造成重载
public class App {
public static void main(String[] args) throws Exception {
ab(30, 20,20);
}
public static void ab(int a,int b) {
int z = a * b;
System.out.println(z);
}
public static void ab(int a,int ac,int b) {
int x = a * ac * b;
System.out.println(x);
}
}
可变参数
当方法的参数列表数据类型已经确定,但是参数个数不确定,就可以使用可变参数。
使用格式:定义方法时使用
修饰符 返回值类型 方法名(数据类型…变量名){}
注意:数据类型和变量名之间要加上三个点。
可变参数的原理:
1、可变参数底层就是一个数组,根据传递参数个数不同,会创建不同长度的数组,来存储这些参数
2、传递的参数个数,可以是0个(不传递),1,2…多个
可变参数的注意事项:
1、一个方法的参数列表,只能有一个可变参数
2、如果方法的参数有多个,那么可变参数必须写在参数列表的末尾
可变参数的终极写法:
public static void method(Object...obj) {}
递归
递归要有递归头和递归体
递归头:什么时候不调用自己。
递归体:什么时候要调用自己。
public static void main(String[] args) throws Exception {
System.out.println(digui(10));
}
public static int digui(int a) {
if (a == 1) {
return 1;//递归头
} else {
return a * digui(a-1);//递归体
}
}
六、数组
特点:
数组中数据类型必须统一!
数组长度在程序运行期间不可改变。
数组初始化
1、动态初始化(指定长度)
int[] name = new int[length];
//动态初始化,存放10个int类型的名叫arrayA的数组
int[] arrayA = new int [10];
2、静态初始化(指定内容)
标准格式:int [] name = new int[]{元素1,元素2……}
//静态初始化,里面存放全部都是int类型的10 ,20 ,30
int[] arrayB = new int []{10 ,20 ,30}
省略格式:int [] name ={元素1,元素2……}
访问数组元素
数组名称[位数(从零开始)]
不管是赋值还是打印都是这样的
获取数组长度
数组名.length
遍历数组
对数组里每个数组的元素进行操作,默认是打印输出
使用for循环,进行遍历
public static void main(String[] args) throws Exception {
int [] array = {10 , 20 , 30 , 40 , 50};
int len = array.length;
for (int i = 0; i < len; i++) {
System.out.println(array[i]);
}
}
数组方法
数组可以作为方法的参数,然后传一个数组类型的实参进去,便可以进行操作。
将数组作为形参
int [] arr ={1, 2 , 3 , 4 , 5 , 6 , 7 , 8};
printArray(arr);
}
public static void printArray(int [] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
将方法作为返回值:
注意,要是是数组作为返回值,则int类型的要写成int[]
public static int[] arrayA(int a , int b , int c) {
int sum = a + b + c;
int [] Array = {sum};
return Array;
}
七、字符串
只要由双引号嵌套的就是字符串
特点
1、字符串的内容不可改变
2、字符串可以共享使用
3、字符串相当于一个char数组,但是底层还是由byte组成
常见3+1种创建方式
一种直接创建:
String four = "ljy";
三种构造方法:
//三种构造方法
String one = new String();//创建一个空白的字符串,啥都没
System.out.println("第一个数组是:" + one);
//依靠char[]创建一个字符串
char[] charArray = {'1', '2' ,'3'};
String two = new String(charArray);
System.out.println("第二个数组是:" + two);
//依靠byte[]创建一个字符串
byte[] byteArray = {97 , 98 , 99};
String three = new String(byteArray);
System.out.println("第三个数组是:" + three);
注意byte[]创造方法返回的是ASCii码的值,char[]构造出来的就是将charArray里面的元素连接起来
常量池
程序中直接创建的值就在字符串常量池中,也只有这种办法才在池中
对于基本类型,==代表的是数值之间的比较
对于引用类型,==代表的是地址值的比较
常量池内存图
equals比较内容(区分大小写)
使用str1.equals(str2(object));来比较str1和str2内容,只有内容完全一样才会返回ture
任何对象都能用object接收
并且具有互斥性 a.equals(b)和b.equals(a)一样,但是要是一个常量一个变量推荐常量在前
String str1 = "Hello";
String str2 = "Hello";
char[] charArray = {'H','e','l','l','o'};
String str3 = new String(charArray);
System.out.println(str1.equals(str2));
System.out.println(str2.equals(str3));
System.out.println(str1.equals(str3));
equalsIgnoreCase比较内容(不区分大小写)
使用方法:具体和equals一样,但是不区分大小写
String str1 = "Hello";
String str2 = "hello";
System.out.println(str1.equalsIgnoreCase(str2));
获取字符串信息
str.length(); 获取字符串长度
System.out.println(str.length());
str.concat(str2); 将str和str2拼接并返回一个新的字符串
String str2 = "qwertyui";
System.out.println(str.concat(str2));
str.charAt(int num); 获取str字符串的索引位为num的字符,并返回
System.out.println(str.charAt(3));
str.indexOf(char); 获取str字符串里第一次出现char的索引位,没有则为-1
System.out.println(str.indexOf("a"));
System.out.println(str.indexOf("fgh"));
截取方法
str demo = str.substring(int index); 从索引位数到index位截取到最后,并且返回一个数组
String demo2 = demo.substring(3);
str demo = str.substring(int beginIndex , int endIndex) 从索引位开始数到beginIndex位,截取到endIndex,并返回一个数组
String demo3 = demo.substring(3, 5);
字符串转换方法
str.toCharArray(); 转换成字符数组,将str拆分成字符
char[] chara = str.toCharArray();
str.getBytes(); 转换成字节数组,将str拆分,存为byte类型,返回ASC码
byte[] bytea = str.getBytes();
str.replace(target, replacement) 将str字符串里所有的target替换成replacement
String repalce = str.replace('l', 'o');
字符串分割方法
str.split(char) 将str以char进行分割,如果是以”.”分割,则需要写成“\\.“(正则表达式)写法,最后并返回一个数组
String[] strArray = str.split(",");
八、类和对象
类:是一组相关属性和行为的集合。可以看作是一类食物的模板,使用事务的属性特征和行为特征来描述该类事物。
成员变量(属性(是什么)):直接定义在类当中,方法的外面
成员方法(行为(能做什么)):成员方法不要写static,直接public void name(){}
通常情况下,一个类并不能直接使用,需要根据一个类创建一个对象才能使用。而创建对象则需要三个步骤:
1.导包:也就是指出要使用的类在什么位置
import 包名称.类名称;
对于当前类是属于同一个包里面则不需要写导包语句。
2.创建:
类名称 对象名 = new 类名称();
3.使用:
成员变量:对象名 .成员变量;
成员方法:对象名 .成员方法名();
当一个对象作为一个参数,传递到方法里面,实际上传递进去的是对象的地址值
面向对象的三大特性
封装
1.方法就是一种封装
2.关键字private(私有化)也是一种封装
继承
1.继承是多态的前提,要是没有继承就没有多态
2.继承主要解决的问题就是:共性抽取
子类可以有父类的内容;字类也可以有自己的独有的内容
通过关键字extends来继承
多态
多态的前提:依靠extends继承和implement来实现
当一个对象有了多种形态,这就是多态,如:
小明是一个学生,但他也是一个人
小明就有两种形态,学生形态和人形态
this
当方法的局部变量和类的成员变量名字相同时,就按照就近原则,如果想要访问类中的成员变量,则需要
this.成员变量名
谁调用方法,this指向的就是谁,并且要在方法的内部写上this
构造方法
就是用来创建对象的一个方法:名称要和类名称一样!!!
public 类名称 (参数类型 参数名称, 参数类型 参数名称){
方法体
}
1.只要new一个对象就是在调用构造方法;
2.构造方法不能有return;
3.构造方法不能写反回值;
4.如果我们没有构造方法,jdk会自动给我们创造一个创造方法
5.一旦编写了至少一个构造方法,jdk就不再帮我们创造构造方法
6.构造方法也是可以重载的
一个标准的类
1、所有成员变量都由private关键字修饰
2、为每一个成员变量都编写一个setter/getter方法
3、编写一个无参数的构造方法
4、编写一个全参数的构造方法
public class dome07javaBean {
//下载Java Code Generators插件
private int age;
private int name;
//右键 然后java generate constructor 生成无参构造函数
public dome07javaBean() {
}
//右键 然后java generate GUI 选择要选中的参数,然后Constructor Using Fields生成全参构造函数
public dome07javaBean(int age, int name) {
this.age = age;
this.name = name;
}
//右键 然后java generate GUI 选择getandset,自动生成
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
public int getName() {
return this.name;
}
public void setName(int name) {
this.name = name;
}
}
匿名对象
直接new 类名().成员变量
new person ().name = “abc”;
每一次匿名都是重新创建一个匿名对象,所以匿名对象只能使用一次。
如果有一个对象只是用唯一的一次,就可以使用匿名对象
static关键字
一旦调用static关键字,则里面的内容不属于对象自己,而是属于类的
变量加上static就是静态变量,方法加上static就是静态方法
一旦使用了static修饰方法,就从成员方法变成了静态方法,调用方式有一点差距
//调用非static方法要先new在调用
App obj = new App();
obj.noStatic();
//调用static方法
App.isStatic();
静态变量 类名称 . 静态变量;
静态方法 类名称 , 静态方法;(对于本类中的静态方法就不用写类名称)
注意:
1.静态不能直接访问非静态。原因:在内存中,是现有静态再有成员,后辈可以访问前辈,但是前辈不知道后辈。
2.静态方法当中不能使用this。
3.根据类名称去访问静态变量,则就和方法没有关系了,只和类有关系
静态代码块
在静态代码块内,无论这个类构造多少方法静态代码块料内的代码只执行一次
static {
System.out.println("无论这个类new多少次都只执行一次");}
private关键字(封装)
私有化成员变量,一旦使用了private,只有本类中可以访问,这时候要是创建一个对象,则无法通过对象来改变private数据,只能自己再在后面创建一个setter和getter方法来间接访问变量
String name;
private int age;
public void setAge(int num){
if (num >= 9 && num <= 100) {
age = num;
} else {
System.out.println("数据不合理");
}
}
public int getAge() {
return age;
}
extends关键字(继承)
在继承关系中,字类就是一个父类,字类可以当作父类来看待
//父类的定义
public class 父类名称 {
}
//字类的定义
public class 字类名称 extends 父类名称{
}
访问特点(多态通用)
直接通过子类对象访问成员变量:
等号左边是谁,就优先用谁,没有则向上找。
间接通过成员方法访问成员变量
方法属于谁就先用谁,没有则往上找
区分子类方法中,三种变量重名的方法
要是子类的方法中,子类方法里的成员变量,子类的成员变量,父类的成员变量都重名则:
//子类方法中的num
System.out.println(num);
//子类中的num
System.out.println(this.num);
//父类中的num
System.out.println(super.num);
访问成员方法的规则
要是子类方法和父类方法重名,则创建的是谁就用谁的成员方法,要是子类没有就用父类的,要是父类(子/父)都没有则报错
无论是成员方法还是成员变量 ,都会向下找,不会向上找
重写(override)
在继承关系中,方法名一样,参数列表也一样,就会造成重写:子类直接把父类层叠掉,覆盖掉
注意:
1、只有方法名,参数列表都相同,才能造成重写
检测重写:在子类要重写的方法前加上@Override然后再写上要重写的子类方法,要是报错就代表重写失败
@Override
public void thisOne() {
}
2、子类返回值必须小于等于父类返回值的范围
3、子类方法的权限修饰符必须大于等于父类方法权限修饰符
public > protected > (default )> private
备注:default不是关键字,而是什么都不写,留空。
继承中构造方法访问特点
1、子类构造方法中有一个默认隐含的“super();”调用,所以一定是先调用的父类构造,后执行子类构造。
2、子类构造可以通过super关键字来调用父类重载构造。
3、super的父类构造调用,必须是子类构造方法的第一个语句。不能一个子类构造调用多次super构造
super关键字
1、在子类方法的成员方法中,访问父类的成员变量。
2、在子类的成员方法中,访问父类的成员方法。
3、在子类的构造方法中 ,访问父类的构造方法
总结:super就是访问父类的
this关键字
1、在本类的成员方法中,访问本类的成员变量
2、在本类的成员方法中,访问本类的另一个成员变量
3、在本类的构造方法中,访问本类的另一个构造方法(this(。。。)调用的也必须是构造方法的第一个语句,唯一一个,super和this这两种方法不能同时使用)
总结:this就是访问本类的
继承三个特点
1、一个子类不可以继承多个父类,一个子类只能有一个父类
2、可以多级继承,一个子类的父类也可以拥有父类,最终都会继承自java.lang.Object
3、一个父类可以有多个子类
现实应用
快捷方法,直接在子类写上要重写的父类方法名,系统自动帮你生成,要是想要在原来的基础上增加方法,则直接在子类的方法里使用super.父类名,将父类方法给引用过来
抽象
比如说你知道这个父类是放动物的,但是你不知道这个动物要吃什么,但是这个动物也一定要吃东西,你只知道他要吃东西,这个就是抽象方法。
抽象类就是在普通的类上加上关键字abstract,抽象方法就是在普通方法的返回值前加上关键字abstract。
public abstract class create {
public abstract void name();
}
抽象类里面可以定义不是抽象方法的方法
1、不能直接创建new抽象类方法。
2、必须用一个子类来抽象类。
3、子类必须覆盖重写父类当中所有的抽象方法
覆盖重写(实现):子类去掉抽象方法的abstract关键字
4、创建子类对象进行使用。
1、抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。
2、抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。(子类构造方法中,有默认的super(),需要访问父类构造方法)
3、抽象类中,可以包含非抽象方法。
4、抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。
多态
代码当中体现多态,其实就是:父类引用指向子类对象
格式:
父类名称 对象名 = new 子类名称();
或者
接口名称 对象名 = new 实现类名称();
多态的成员方法
访问方法:
看等号的右边是谁,new出来的是谁就用谁的方法,没有则向上找
口诀:
编译看左边,运行看左边
多态成员变量
访问成员变量的两种方式:
1、直接通过对象名称访问成员变量:看等号左边是谁,优先用谁,没有则向上找。
2、间接通过成员方法访问:通过调用方法,然后调用子类的方法来简介访问成员变量
口诀:
编译看左边,运行看右边
多态的好处
对象的上下转型
向上:
对象的向上转型,其实就是多态写法:
格式:父类名称 对象名 = new 子类名称();
含义:右侧创建一个子类对象,把它当作父类来看待使用。
注意事项:向上转型一定是安全的。因为他是从小范围转换到大范围
弊端:子类不能在增加方法(子类不能有特有方法),一旦向上转型位父类,那么就无法调用子类原本特有方法。
向下:
对象的向下转型,其实就是一个还原的动作。
格式:子类对象 对象名 = (子类名称)父类对象(这里指的父类对象是向上转型的对象名);
含义:将父类对象,还原成原来的子类对象。
注意事项:
1、必须保证对象本来创建的时候就是A,才能向下转型成为A。
2、如果对象创建的时候本来不是A,现在非要向下转型成为A,就会报错。
Instanceof关键字
判断对象是否属于某种类型的(判断向上转型的是否属于子类类型)
格式:
对象名 instanceof 类型
会返回true和false
通常使用会将Instanceof使用if包装一下
if (one instanceof Zi) {
Zi two = (Zi) one;
System.out.println(two.num);
}
final关键字
final关键字代表最终,不可改变的
四种用法:
1、用来修饰一个类
2、用来修饰一个方法
3、用来修饰一个局部变量
4、用来修饰一个成员变量
用来修饰类:
格式:
public final class 类名称{}
含义:当前这个类不能有任何子类
注意事项:一个类如果被final修饰了,那么其中的所有成员方法都无法进行覆盖重写(因为没子类)
用来修饰方法:
格式:
修饰符 final 返回值类型 方法名称(参数列表){};
含义:当final关键字用来修饰一个方法的时候,这个方法就是最终方法,不能被覆盖重写
注意事项:对于类、方法来说,abstract关键字和final关键字不能同时使用,因为矛盾。
用来修饰局部变量:
格式:
final 数据类型 数据名 = 数据值;
含义:一旦使用final用来修饰局部变量,那么这个变量就不能进行更改。
注意事项:
1、对于基本类型来说,不可变说的是变量当中的数据不可改变。
2、对于引用类型来说,不可变说的是当变量中的地址值不可改变。
用来修饰成员变量:
格式:
final 数据类型 数据名 = 数据值;
含义:
注意事项:
1、由于成员变量具有默认值,所以用了final后必须手动赋值,不会再给默认值。
2、对于final的成员变量,要么直接赋值,要么通过构造方法赋值。二者选其一。
3、必须保证类当中所有重载的构造方法,都最终会对final的成员变量进行赋值
四种权限修饰符
public > protected > (default) > private 这里的default不是关键字default,而是什么都不写
public | protected | (default) | Private | |
同一个类 | 可以 | 可以 | 可以 | 可以 |
同一个包 | 可以 | 可以 | 可以 | 不可以 |
不同包子类 | 可以 | 可以 | 不可以 | 不可以 |
不同包非子类 | 可以 | 不可以 | 不可以 | 不可以 |
内部类
就是一个类的内部包含另一个类
分类:
1、成员内部类
2、局部内部类 包含(3、匿名内部类)
成员内部类:
修饰符 class 外部内部类{
修饰符class 内部类名称{
//。。。
}
}
注意:内用外,随意访问;外用内,需要内部类对象。
1、间接方式:在外部类的方法当中,使用内部类,然后main只是调用外部类的方法。
public class Innerdemo01 {
public void two() {
System.out.println("内部类方法");
}
}
public void one() {
System.out.println("外部类方法");
//用外部内方法调用内部类(间接访问)
new Innerdemo01().two();
}
2、直接方法:
一般情况下是:
类名称 对象名 = new 类名称();
有内外部类:
外部类.内部类 对象名 = new 外部类() . new 内部类();只能访问内部类
// 直接访问
// 公式:外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();
demo01.Innerdemo01 dm2 = new demo01().new Innerdemo01();
public class VariableDuplicateName {
int num = 10; //外部类成员变量
public class inter {
int num = 20; //内部类成员变量
public void numm() {
int num = 30; //内部类局部变量
System.out.println(num); //访问内部类局部变量
System.out.println(this.num); //访问内部类成员变量
System.out.println(VariableDuplicateName.this.num); //访问外部类成员变量
}
}
}
如果出现重名现象,那么格式就是:外部类名称 . this . 外部类成员变量名
局部内部类:
如果一个类是定义在一个方法内部的,那么这就是一个局部内部类。
“局部”:只有当前所属的方法才能使用它,除了这个方法外面就不能使用
修饰符 class 外部类名称{
修饰符 返回值类型 外部类方法名称(参数列表){
class 局部内部类名称{ }
} }
public class demo03LocalInnerClass {
public void out() {
class inter {
}
}
}
只能在局部里面使用,出了包裹它的方法就是用不了了,但是你可以在其他的类里面调用包裹它的方法
在局部内部类,如果希望所访问所在方法的局部变量,那么着局部变量必须是有效final的。(从JDK 8 开始,只要内部变量不变,那么final关键字可以省略)
原因:
1、new出来的对象是在堆内存当中。
2、局部变量是跟着方法走的,在栈内存当中。
3、方法运行结束之后,会立刻出栈,局部变量就会立刻消失。
4、但是new出来的对象会在堆当中持续存在,直到垃圾回收。
匿名内部类(重点)
如果接口的实现类(或者是父类的子类)只需要使用唯一的一次,那么这种情况下就可以省略掉该类的定义,而改为使用【匿名内部类】,可以让我们省略一个实现类
接口名称 对象名 = new 接口名称(){
//覆盖重写所有抽象方法
}
对格式“new 接口名称(){…}“解析:
1、new代表创建对象的动作。
2、接口名称就是匿名内部类需要实现哪个接口。
3、{…}这才是匿名内部类的内容。
1、匿名内部类,在创建对象的时候,只能使用唯一一次。(如果希望多次创建对象,并且类的内容一样的话,那么就必须使用单独定义的实现类了。)
2、匿名对象,在调用方法的时候,只能调用唯一一次。(如果希望同一个对象,调用多次方法,那么就必须给对象起个名字)
3、匿名内部类是省略了实现类/子类名称,但是匿名对象是省略了对象名称。(强调:匿名内部类和匿名对象不是一回事)
类的创建与权限修饰符
创建类的时候可以使用的权限修饰符
1、外部类:public / (default)
2、成员内部类:public / protected / (default) / private
3、局部内部类:什么都不能写
小知识:
1、可以将类作为成员变量的类型
2、可以将接口作为成员变量的类型
九、API的使用
除了java.lang类里面不需要导包,其他都需要导包
导包:
import 包路径 . 类名称
如果要使用的目标类和当前位于同一个包下,则导包语句可以不写
创建
类名称 name = new 类名称();
使用
name 对象方法名();
API里写上<E>,指的是泛型
泛型:装在集合里的所有元素都是统一的某种类型,但是泛型只能是应用类型,不能是基本类型,如果希望是基本类型,必须使用基本类型的包装类
常用类
Scanner
import java.util.Scanner;
放在main里面
Scanner s = new Scanner(System.in);
s.nextInt();//调用方法输入可以转换成char或者float等等
nextLine 则代表输入的一行文字
Random
产生随机数字
public static void main(String[] args) {
Random rd = new Random();
int a = rd.nextInt();
System.out.println(a);
}
要是调用的时候括号留空,则代表整个范围,要是有参数的话,则是左闭右开区间,必须要两个参数
Arrays
Arrays是一个与数组相关的工具类,里面提供了很多与数组相关的静态方法,用来实现数组常见的操作,直接调用,不需要new,直接Arrays.xxx();
Arrays.toString(arr); 将参数数组变成字符串(默认格式[元素1,元素2…])
int[] intArray = {1, 2, 3, 4, 5, 6};
String intArrToString = Arrays.toString(intArray);
System.out.println(intArrToString);//[1, 2, 3, 4, 5, 6]
Arrays.sort(arr) 对数组重新排序,默认从小到大
如果是数值,则默认按照升序从小到大排序
如果是字符串,则默认按照字母升序
如果是自定义类型,那么这个自定义的类需要有Comparable或者Comparator接口支持
int[] sortArr = {5, 8, 2, 1, 10};
Arrays.sort(sortArr);
System.out.println(Arrays.toString(sortArr));//[1, 2, 5, 8, 10]
Math
是数学相关的工具类,里面提供了大量的静态方法,完成与数学运算相关的工作
Math.abs () 取绝对值;
Math.ceil() 向上取整
Math.floor() 向下取整
Math.round() 四舍五入
Object
1、toString
2、equals
指示其他某个对象是否和此对象相等
equals方法源码:
public boolean equals(Object obj) {
return (this == obj);
}
参数:Object obj 可以传递任意对象;
里面的this指的就是调用它的方法
返回值:boolean 返回值是true和false
基本数据类型:比较的是值;
引用数据类型:比较两个对象的地址值;
3、hashCode
返回一个元素的哈希值
Objects的equals方法:
一个工具包,里面有很多方法,可以有效防止空指针异常
Date
表示日期和时间的类
类 Date 表示特定的瞬间,精确到毫秒。
Ststem.currentTimeMillis 查看当前时间距离1970.1.1多少毫秒
构造方法:
分成两种:
不带参
Date date = new Date();
//Date类空参数构造方法 获取当前日期和时间
System.out.println(date);//Wed Feb 05 16:37:28 CST 2020
带参
Date date = new Date(0l);
//0毫秒所属于的时间(注意传参要传一个long类型的进去)
System.out.println(date);
成员方法:
getTime(); 返回1970到当前时间的毫秒值
DateFormat
是日期/时间格式化子类的抽象类(所以无法直接创建对象使用,可以使用他的子类来操作SimpleDateFormat )
作用:格式化(日期à文本)、解析(文本à日期)
构造方法:
SimpleDateFormat dateformate = new SimpleDateFormat(pattern);
pattern指的是默认的格式(y 年 ,M 月 ,d 日 ,H 时 ,m 分,s 秒),写对应的模式,会把模式替换为对应的日期和时间“yyyy-MM-dd HH:mm:ss”
模式中的子母不能改变,但是连接模式的符号可以改变
成员方法:
1、format(格式化日期à文本)
//首先创建一个对象,将格式传递进去
SimpleDateFormat demo = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//new一个无参的date,获取当前时间
Date date = new Date();
//进行格式化,并且用一个变量接收
String riqi = demo.format(date);
System.out.println(riqi);
2、parse(解析文本à日期)
如果字符串和构造方法不一样,那么程序就会抛出此异常,调用一个抛
出异常的方法,就必须处理这个异常,要么throws继续抛出,要么try catch自己处理。
//调用throws继续抛出
public static void Dateparse() throws ParseException {
//首先创建一个对象,将格式传递进去
SimpleDateFormat demo = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
//调用方法parse,把符合构造方法中模式的字符串解析为Date
Date date = demo.parse("2020年02月05日 17时47分36秒");
System.out.println(date);
}
Calendar
日历类,出现在Date之后,替换掉了很多Date方法
calendar是一个抽象类,无法直接创建对象使用,里面有一个静态方法叫做getInstance(),该方法返回Calendar类的子类对象
Calendar c = Calendar.getInstance();//多态
关键成员方法
成员方法的参数:
int field:日历类的字段,可以使用calendar类的静态成员变量获取
get(int field) 返回给定日历字段的值
//使用getInstance来获取calendar对象
Calendar c = Calendar.getInstance();
//调用get方法
System.out.println(c.get(1));//年
System.out.println(c.get(2));//月(从0开始算)
System.out.println(c.get(5));//日
System.out.println(c.get(10));//时
System.out.println(c.get(12));//分
System.out.println(c.get(13));//秒
set(int field,int amount) 将给定的日历字段设置为给定值
//使用getInstance来获取calendar对象
Calendar c = Calendar.getInstance();
//使用set方法,将年设置为9999年
c.set(Calendar.YEAR, 9999);
//查看设置的年
System.out.println(c.get(Calendar.YEAR));
//同时设置年月日
c.set(8888, 8, 8);
add(int field,int amount) 更具日历的规则,位给定的日历字段添加或减去指定的时间量。(把指定的字段增加减少指定的值)
//使用getInstance来获取calendar对象
Calendar c = Calendar.getInstance();
//用add方法给年增加一位(正数是加,负数是减)
c.add(Calendar.YEAR, 1);
//查看年
System.out.println(c.get(Calendar.YEAR));
getTime() 返回一个表示calendar的时间值(从历元到现在的毫秒偏移值)的Date对象。
//使用getInstance来获取calendar对象
Calendar c = Calendar.getInstance();
//将日历对象转换成日期对象,用date来储存
Date date = c.getTime();//Thu Feb 06 17:43:26 CST 2020
System.out.println(date);
System
system类中提供了大量的静态方法,可以获取与系统相关的信息或系统级操作
常用方法有:
currentTimeMillis(); 返回以毫秒为单位的当前时间。
//返回以毫秒为单位的当前时间
System.out.println(System.currentTimeMillis());
arraycopy(Object src,int srcPos,Object dest,int destPos,int length) 将数组中指定的数据拷贝到另一个数组中。
//src - 源数组。
//srcPos - 源数组中的起始位置。
//dest - 目标数组。
//destPos - 目标数据中的起始位置。
//length - 要复制的数组元素的数量。
System.arraycopy(src, srcPos, dest, destPos, length);
StringBuilder
又叫字符串缓冲区,字符串是常量,不能修改,但是字符串缓冲区支持可变的字符串,可以提高字符串的效率
构造方法:常用第一个和第四个
常用方法:
append() 添加任意类型数据的字符串形式,并返回当前对象自身。
StringBuilder sb = new StringBuilder();
//使用append方法往里面增加字符串
//append返回的是this,调用方法的对象
StringBuilder sb2 = sb.append("sajaklsfjalksfjalksfjal");
System.out.println(sb);
System.out.println(sb2);
System.out.println(sb == sb2);//true//比较的是地址值
//使用append方法不需要接收值
因为append返回的是this,所以可以使用链式编程
toString() 将当前StringBuilder对象转换为String对象
使用含参构造方法StringBuilder(String str)可以将String转化成StringBuilder对象,而是用toString(),则是将StringBuilder对象转换成String对象
String str = "ljy";
System.out.println(str);
StringBuilder sb = new StringBuilder(str);
//对字符串进行改变
sb.append("Lvadnjy");
//使用String进行接收
String str2 = sb.toString();
System.out.println(str2);
append和toString方法相互使用,则可以实现字符串的转换
Collection<E>集合
集合是java提供的一种容器,可以用来存储多个数据。
区别:
1、数组的长度是固定的。集合的长度是可变的。
2、数组中存储的是同一类性元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,就会使用集合进行存储。
Collenction共性方法
Collenction(采集)的共性方法:即父类,子类都有的方法
public boolean add(E e) 把给定的对象添加到当前集合中。
public void clear() 清空集合中所有的元素
public boolean remove(E e) 把给定的对象在当前集合中删除。
public boolean contains(E e) 判断当前集合中是否包含给定的对象
public boolean isEmpty() 判断当前集合是否为空。
public int size() 返回当前集合的个数
public Objcet[] toArray() 把集合中的元素存储到数组中
迭代器
在集合中,我们经常需要进行遍历,但是每一种集合的遍历方式都不一样,所以JDK专门提供了一个接口Iterator(迭代器)Iterator主要用于迭代访问(遍历)Collection元素
迭代:即Collection集合元素的通用获取方式。在去元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续判断,如果还有就在取出来,一直把集合中所有元素全部取出,这种取出方式专业 属于称之为迭代。
Iterator迭代器是一个接口,我们无法直接使用,需要使用Iterator接口的实现类对象,获取实现类的方法比较特殊,在Collection接口中有一个方法叫做iterator(),这个方法返回的就是迭代器的实现类对象
Iterator常用方法:
public E next();返回迭代的下一个元素;
public boolean hasNext(); 如果任有元素可以迭代,则返回true
迭代器的使用步骤:
1、使用集合中的方法iterator()获取迭代器的实现类对象,使用Iterator接口接收(多态)。
// 创建一个集合
Collection<String> coll = new ArrayList<>();
//获取迭代器
Iterator<String> it = coll.iterator();
2、使用Iterator接口中的方法hasNext判断还有没有下一个元素。
//使用hasNext
boolean a = it.hasNext();
3、使用Iterator接口中的方法next取出集合中的下一个元素。
//使用next拿出来
String str = it.next();
当不知道还有多少个的时候便可以使用while循环来进行代码美化
while (it.hasNext()) {
String str = it.next();
System.out.println(str);
}
所有的单列循环都可以使用foreach循环
格式:
for(集合/数组的数据类型 变量名 : 变量名/数组名){
sout(变量名);
}
for (String str : coll) {
System.out.println(str);
}
泛型
泛型就是指的是一种未知的数据类型,当要调用带有泛型的方法的时候,再将泛型作为参数传递
不使用泛型:
好处:集合不使用泛型,默认的类型就是Object,可以储存任意类型的数据
弊端:不安全,会引发异常
使用泛型:
好处:1、避免了类型转换的麻烦,存储的是什么类型,取出的就是什么类型
2、把运行期的异常(代码运行之后会抛出的异常),提升到了编译期(写代码的时候会报错)
弊端:泛型是什么类型,就只能存储什么类型的数据
定义和使用泛型的类:泛型定义在类名之后
public class demo01class<E> {
private E name;
public demo01class() {
}
public demo01class(E name) {
this.name = name;
}
public E getName() {
return name;
}
public void setName(E name) {
this.name = name;
}}
在new的时候 就像ArrayList一样在创建的时候定义泛型类容,不定义则是Object类型。
定义和使用泛型的方法:泛型定义在修饰符和反回值之间,参数类型也要使用泛型
public <E> void method(E name) {
System.out.println(name);
}
定义和使用含有泛型的接口:泛型定义在接口名的后面,但是实现类的时候就要直接确定泛型是什么类型的,不然会报错
public interface demo03Interface<E> {
public abstract void method(E e);
}
//第一种方法:直接实现类确定什么类型的泛型,创建的时候也就只能创建什么泛型
public class demo03InterfaceImpl implements demo03Interface<Integer>{}
//第二种方法:在new的时候由使用者在确定的时候确定泛型
public class demo03InterfaceImpl<E> implements demo03Interface<E>{}
泛型通配符:
?:代表任意的数据类型。
使用方式:
不能创建对象使用
只能作为方法的参数使用
public static void main(String[] args) {
//定义一个集合,存放一种数据类型
ArrayList<Integer> demo1 = new ArrayList<>();
demo1.add(1);
demo1.add(2);
demo1.add(3);
//定义第二个集合,存放不一样的数据类型
ArrayList<String> demo2 = new ArrayList<>();
demo2.add("ljy");
demo2.add("zjl");
demo2.add("zql");
//运行printArray方法
printArray(demo1);
printArray(demo2);}
//定义一个方法,要实现所有的集合都能遍历,不管传什么数据类型的泛型都要能够遍历,则使用泛型通配符<?>
public static void printArray(ArrayList<?> list) {
//接受类型因为不知道是什么,所以直接选择Object类型,无论什么类型都可以接收
for (Object one : list) {
System.out.println(one);}}
List集合
特点:
1、有序的集合,存储元素和取出元素是一致的
2、有索引值的,包含了一些索引的方法
3、与set不同,List集合允许存储重复的数据
public void add(int index , E element):将特定的元素,添加到该集合中指定的位置上
public E get(int index):返回集合中指定位置的元素。
public E remove(int index):移除列表中指定位置的元素,返回值是被移植的元素。
public E set(int index , E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
一般情况下数组的长度不能发生改变,但是ArrayList数组的长度可以发生改变
假如要创建String类型的数组,则要写成
ArrayList<String> list = new ArrayList<>();
然后调用add方法,往list里面添加元素
list.add("ljy");
获取 get方法,参数填索引值,从零开始算
String ll = list.get(1);
删除 remove方法,参数索引值
String m = list.remove(2);
长度 size,返回值是数组长度
System.out.println(list.size());
想要导入基本类型(byte short Integer long float double char boolean)到泛型里面去则需要调用包装类
特点:增删慢,查找快(多线程)
特点:查询慢,增删快(多线程),有大量的操作首尾元素的方法(注意 使用LinkedList集合特有方法,不能使用多态)
构造方法:
LinkedList<泛型> name = new LinkedList<>();
LinkedList<String> list1 = new LinkedList<>();
常用方法:
(添加)
public void addFirst(E e); 将指定元素插入此类列表的开头
public void addLast(E e); 将指定元素添加到此列表的结尾
public void push(E e); 此方法等效于addFirst();
(查找,获取)
public E getFirst(); 返回此列表的第一个元素
public E getLast(); 返回此列表的最后一个元素
(删除)
public E removeFirst(); 移除并返回此列表的第一个元素
public E removeLast(); 移除并返回此列表的最后一个元素
public E pop(); 此方法等效于removeFirst();
(检查)
public boolean isEmpty(); 如果此列表不包含元素,则返回true
(清空)
public void clear(); 清空集合里所有的元素
已经被ArrayList集合取代
Set集合
特点:
1、不允许存储重复的元素
2、没有索引,没有带索引的方法,也不能使用普通的for循环遍历
方法:和collection的方法一致
Set集合存储元素不重复的原理:
假设要将demo使用add方法添加到set集合,则首先add方法会先调用demo
的hashCode方法,计算demo的哈希值,然后再set中查找有没有哈希值一样的元素,如果没有,就会把demo存储到集合中,但是要是demo2和demo哈希冲突,系统就会自动调用equals方法,要是equals方法返回为falsh,则认定是哈希冲突,才会把demo2存储到set类里面
特点:
1、不允许存储重复的元素
2、没有索引,没有带索引的方法,也不能使用普通的for循环遍历,可以使用迭代器遍历
3、是一个无序的集合,存储元素和取出元素的顺序可能不一致
4、底层是一个哈希表结构(查询速度非常快)
创建方法:
//使用多态创建
Collection<Integer> set = new HashSet<>();
哈希值:是一个十进制的整数,由系统随即给出(就是对象的地址值,是一个逻辑地址,是模拟出来得到地址,不是数据实际存储的物理地址),在Object类有一个方法(hashCode()),可以获取对象的哈希值
demo d1 = new demo();
int a = d1.hashCode();
一般方法下的哈希值都是十进制的,但是String类重写了哈希值的方法,他返回的是十六进制的
HashSet集合存储数据的结构(哈希表):
JDK1.8之前:哈希表 = 数组 + 链表;
JDK1.8之后:哈希表 = 数组 + 链表;
哈希表 = 数组 + 红黑树;(如果链表的长度超过了8位,那么就会把链表转换为红黑树(提高查询速度))
数据结构:把元素进行分组(相同哈希值的元素是一组),链表/红黑树结构把相同哈希值的元素连接在一起。
hashSet存储自定义类型元素必须重写hashCode方法和equals方法(不重写就是比较地址值)
LinkedHashSet继承HashSet,特点:底层是一个哈希表(数组+链表/红黑树)+链表(多了一条链表,记录元素的存储顺序,保证元素有序。
方法和创建方法都和HashSet一样。
Collections集合工具类的方法
常用方法:
public static <T> boolean addAll(Collection<T> c, T…elements) 忘记何种添加一些元素。
public static void shuffle(List<?> list) 打乱顺序:打乱集合顺序
//创建一个集合
ArrayList<Integer> dm = new ArrayList<>();
//使用addAll方法往里面添加很多元素
Collections.addAll(dm, 1 , 2, 3 , 4 , 5 );//第一个是数组名,后面则是要添加的元素
System.out.println(dm);//[1, 2, 3, 4, 5]
//对元素进行打乱
Collections.shuffle(dm);
System.out.println(dm);//[5, 1, 4, 3, 2]
public static <T> void sort(List<T> list) 将集合中元素按照默认规矩排序(升序)
注意:sort方法实现的前提是被排序的集合里边存储的元素,必须继承Comparable,重写接口中的CompareTo定义排序规则
public class Person implements Comparable<Person>{
@Override
public int compareTo(Person o) {
// return 0;//认为元素都是相同的
//自定义比较规则,比较两人的年龄(this,参数Person)
//o.getAge() - this.getAge() 年龄的降序排序
//this - 参数就是升序 反过来就是降序
return this.getAge() - o.getAge();//年龄升序排序
}
public static <T> void sort(List<T> list,Comparator<? super T>) 将集合中元素按照指定规则排序(作为了解足以)
Comparator和Comparable的区别:Comparable:自己(this)和别人(参数)比较,自己需要实现Comparable接口,重写比较的规则compareTo方法;Comparator:相当于找一个第三方裁判,比较两个
//对象的sort
ArrayList<Person> dm2 = new ArrayList<>();
//创建对象并放进集合
dm2.add(new Person("ljy", 18));
dm2.add(new Person("zql", 19));
dm2.add(new Person("sh", 17));
System.out.println(dm2);//[Person [age=18, name=ljy], Person [age=19, name=zql], Person [age=17, name=sh]]
//按照自己定义的方法来排序,使用Comparator匿名内部类来书写
Collections.sort(dm2, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
//按照年龄的升序
return o1.getAge() - o2.getAge();
}
});
System.out.println(dm2);//[Person [age=17, name=sh], Person [age=18, name=ljy], Person [age=19, name=zql]]
Map<K,V>集合
一个Map集合包含两个泛型,其中K是键,V是值(K代表key,V代表value),一个映射不能包含重复的键,每个键最多只能映射到一个值,但是键是唯一的,值是可以重复的,并且K和V的数据类型是可以不重复的
常用子类:
HashMap集合:
1、HashMap集合底层是哈希表,查询速度很快,
JDK1.8之前是数组+单向链表。
JDK1.8之后:数组+单向链表/红黑树(链表长度超过8就是红黑树)
2、HashMap是一个无序的集合,存储元素和取出元素的顺序有可能不一致(会通过key进行升序排序,假如放进去的是3、1、2,但是在里面排序会使1、2、3)
LinkedHashMap集合:
1、LinkedHashMap集合底层是哈希表+链表(保证迭代顺序)
2、LinkedHashMap集合是个有序的集合,存储元素和取出元素的顺序是一致
MAP的常用方法:
public V put(K key, V value):把指定的键与指定的值添加到Map集合中
返回值是v,要是k在此之前是没有v的则返回null,v是覆盖的话,则返回原来的v
public V remove(Object key):通过K将键值删除
K存在,则返回k对应的值;要是K不存在,则返回null
public V get(Object key):根据指定的键,在Map集合中获取对应的值。
boolean containsKey(Object key):判断集合中是否包含指定的键。
对Map元素进行遍历:
键找值:
先使用keySet将Map集合中所有的Key取出来存储到Set集合里面,在通过foreach循环遍历Set集合,获取每一个Key,最后在使用get(Key)方法获取value
public set<K> keySet():获取Map集合中所有的键(Key),存储到Set集合中。
//遍历Map集合里面的元素 直接让增强for循环的目标数组是map.keyset()
for (String str : map.keySet()) {
//使用get(Key)找出key所对应的v
System.out.println(map.get(str));
}
键值对:
public Set<Map.Entry<K,V>>entrySet():获取到Map集合中所有的键值对(一对K,V)对象的集合(Set集合)
Entry集合有两个方法,getKey()和getValue();获取键和值
//先使用entrySet方法将map的键值对存放在entry格式的Set集合里
Set<Entry<String,Integer>> set = map.entrySet();
//对Set集合进行遍历
for (Entry<String,Integer> ent : set) {
System.out.println(ent);
}
System.out.println("=============");
// 也可以直接快速方式书写
for (Entry<String,Integer> ent : map.entrySet()) {
System.out.println(ent);
}
HashMap
使用HashMap存储自定义键值对:
Map集合必须要保证Key是唯一的,所以当Key和Value是类的时候,就必须要重写hashCode和equals方法,以确保Key的唯一
public static void main(String[] args) {
//自定义Key和Value需要防止Key值是重复的,所以Key是要重写hashCode和equals方法,以确保Key的唯一
//以自定义类型为Value
demo01();
//以自定义类型为Key
demo02();
}
private static void demo02() {
//创建一个map集合,其中key是ClassKey类,value是String
HashMap<ClassKey,String> map = new HashMap<>();
map.put(new ClassKey("ljy",18), "黄冈");
map.put(new ClassKey("zjl",18), "武汉");
map.put(new ClassKey("zql",18), "湖北");
map.put(new ClassKey("ljy",18), "黄冈");
//使用foreach循环遍历
for (ClassKey ck : map.keySet()) {
String dz = map.get(ck);
System.out.println(ck + "=" + dz);
}
//因为没有重写ClassKey的hashCode和equals方法,所以这时候就算k是一样的也不会覆盖
}
public static void demo01() {
//创建一个map集合,其中key是String,value是ClassKey类
HashMap<String,ClassKey> map = new HashMap<>();
map.put("黄冈", new ClassKey("ljy",18));
map.put("武汉", new ClassKey("zjl",18));
map.put("湖北", new ClassKey("zql",18));
map.put("黄冈", new ClassKey("sh",18));
//使用增强for来遍历
for (String key : map.keySet()) {
ClassKey value = map.get(key);
System.out.println(key + "=" + value);
}
//因为key相同了,所以sh将ljy覆盖了
}
继承了HashMap集合,map接口的哈希表和链接列表实现,具有可预知的迭代顺序。
底层原理:哈希表+链表(记录元素的顺序)
//创建一个普通的HashMap集合
HashMap<Integer,Integer> map = new HashMap<>();
map.put(1, 10);
map.put(4, 40);
map.put(2, 20);
map.put(3, 30);
System.out.println(map);//{1=10, 2=20, 3=30, 4=40}
//打印出来和放进去的不一样
//创建一个LinkedHashMap集合
Map<Integer,Integer> lmap = new LinkedHashMap<>();
lmap.put(1, 10);
lmap.put(4, 40);
lmap.put(2, 20);
lmap.put(3, 30);
System.out.println(map);//{1=10,4=40 ,2=20, 3=30}
//打印出来和放进去顺序 是一样
HashTable
最早期的双链集合,单线程(速度慢),不能存储null键,null值,已经被HashMap取代,但是HashTable的子类Properties依然活跃在历史的舞台
Properties集合是一个唯一和IO流结合的集合
File
文件和目录路径名的抽象表示形式。
java把电脑中的文件和文件夹(目录)封装为一个File类,我们可以使用File类对文件和文件夹进行操作
我们可以使用File类的方法可以:
创建一个文件/文件夹
删除文件/文件夹
获取文件/文件夹
判断文件/文件夹是否存在
对文件夹进行遍历
获取文件的大小
File类是一个与系统无关的类,任何的操作系统都可以使用这个类中的方法
重点:三个单词 file(文件)、directory(文件夹/目录)、path(路径)。
分隔符
四个静态方法
static String pathSeparator 与系统有关的路径分隔符,为了方便,他被表示为一个字符串
static char pathSeparatorChar 与系统有关的路径分隔符。
static String separator 与系统有关的默认名称分隔符,为了方便,他被表示为一个字符串
static char separatorChar 与系统有关的默认名称分隔符
操作路径:路径不能写死了
"C:\develop\a\a.txt" windows
"C:/develop/a/a.txt" linux
"C:"+File.separator+"develop"+File.separator+"a"+File.separator+"a.txt"
路径
路径分为两种路径:
绝对路径 : 是一个完整的路径
以盘符(c:,D:)开始的路径
c:\\a.txt
C:\\Users\itcast\\IdeaProjects\\shungyuan\\123.txt
D:\\demo\\b.txt
相对路径 : 是一个简化的路径
相对指的是相对于当前项目的根目录(C:\\Users\itcast\\IdeaProjects\\shungyuan)
如果使用当前项目的根目录,路径可以简化书写
C:\\Users\itcast\\IdeaProjects\\shungyuan\\123.txt-->简化为: 123.txt(可以省略项目的根目录)
注意:
1.路径是不区分大小写
2.路径中的文件名称分隔符windows使用反斜杠,反斜杠是转义字符,两个反斜杠代表一个普通的反斜杠
构造方法
有四种构造方法:
File(File parent, String child)
根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。
File(String pathname)
通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。
File(String parent, String child)
根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
File(URI uri)
通过将给定的 file: URI 转换为一个抽象路径名来创建一个新的 File 实例。
String pathname:字符串的路径名称
路径可以是以文件结尾,也可以是以文件夹结尾
路径可以是绝对路径,也可以是相对路径
路径可以是存在,也可以是不存在
创建File对象,只要把文件夹路径封装为File对象,不考虑路径的真假情况
把路径分成了两部分
String parent 父路径
String child 子路径
好处:
父路径和子路径都可以单独书写,使用起来非常灵活;父路径和子路径都可以变化。
把路径分成了两部分
File parent 父路径
String child 子路径
好处:
父路径和子路径都可以单独书写,使用起来非常灵活;父路径和子路径都可以变化。
父路径是File类型,可以使用File的方法对路径进行一些操作,在使用路径创建对象。
File类中的常用方法
File类中的常用方法分成三类:获取方法,判断方法,创建删除方法
public String getAbslutePath() 返回此File的绝对路径名字符串
获取构造方法中传递的路径,无论路径是绝对路径还是相对路径,getAbslutePath都会返回绝对路径
//创建一个构造方法,传入一个路径
File demo01 = new File("G:\\Java\\API\\File\\src\\Method");
String lj = demo01.getAbsolutePath();
// 无论路径是绝对的还是相对的,getAbsolutePath方法返回的都是绝对路径
System.out.println(lj);
public String getPath() 将此File转换为路径名字符串
获取构造方法中传递的路径,是绝对路径就返回绝对路径,是相对路径就返回相对路径
File demo01 = new File("G:\\Java\\API\\File\\src\\Method");
System.out.println(demo01.getPath());
File demo02 = new File("path.exe");
System.out.println(demo02.getPath());
public String getName() 返回由此File表示的文件或目录的名称
获取的就是构造方法传递路径的结尾部分(文件/文件夹)
File demo01 = new File("G:\\Java\\API\\File\\src\\Method");
System.out.println(demo01.getName());//Method
public long length() 返回由此File表示的文件的长度(大小)
获取到的是构造方法指定的文件大小,以字节为单位
注意:
文件夹是没有大小概念的,不能获取文件夹的大小
如果构造方法中给出的路径不存在,那么length方法返回0
File demo01 = new File("G:\\Java\\API\\File\\src\\Method\\GetMethod\\demo.java");
System.out.println(demo01.length());//2584
public boolean exists() 此File表示的文件或目录是否实际存在
用于判断创造方法中的路径(绝对、相对都可以)是否存在
存在:true
不存在:false
注意:相对路径是以此项目的根目录为起始,并不是以此Java文件为起始
File demo01 = new File("G:\\Java\\API\\File\\src\\Method");
System.out.println(demo01.exists());
File demo02 = new File("..\\System");
System.out.println(demo02.exists());
public boolean isDirectory() 此File表示的是是否为目录
用于判断构造方法中给定的路径是否以文件夹结尾
是:true
否:false
public boolean isFile() 此File表示的是否为文件
用于判断构造方法中给定的路径是否以文件结尾
是:true
否:false
注意:
电脑的硬盘中,只有两种存储方式(文件/文件夹),所以两种方法是互斥的
并且路径必须是存在的,不然都返回false
File demo01 = new File("G:\\Java\\API\\File\\src\\Method");
//先判断路径是否存在,不存在则没必要获取
if (demo01.exists()) {
System.out.println(demo01.isDirectory());
System.out.println(demo01.isFile());
}
public boolean createNewFile() 当且仅当具有该名称的文件尚不存在时,创建一个新的空文件
创建文件的路径和名称在构造方法中给出(构造方法的参数)
返回值:
true:文件不存在,创建成功
false:文件存在,不会创建
注意:
1、此方法只能创建文件,不能创建文件夹
2、创建文件的路径必须存在,否则会抛出异常
// 因为可能会报错,所以要么trycatch要么直接throws
// 创建就直接创建的是File后面构造方法的最后一个路径名称
File file = new File("G:\\Java\\API\\File\\src\\Method\\create.txt");
boolean xg = file.createNewFile();//绝对路径创建
System.out.println(xg);
File file2 = new File("src\\method\\create2.txt");
boolean xg2 = file2.createNewFile();//相对路径创建
System.out.println(xg2);
public boolean delete() 删除由此File表示的文件或目录
此方法,可以删除构造方法路径中给出的文件/文件夹
返回值:
true:文件/文件夹删除成功,返回true
false:文件夹中有内容,不会删除返回false,构造方法中路径不存在false
注意:
delete方法是直接在硬盘中删除文件/文件夹,不走回收站,删除要谨慎
File file = new File("G:\\Java\\API\\File\\src\\Method\\aaa");
boolean xg = file.delete();
System.out.println(xg);
File file2 = new File("G:\\Java\\API\\File\\src\\Method\\111\\222\\333\\444");
boolean xg2 = file2.delete();
System.out.println(xg2);
public boolean mkdir() 创建由此File表示的目录(创建单级文件夹)
public boolean mkdirs() 创建由此File表示的目录,包括任何必须但不存在的父目录(既可以单级文件夹,也可以多级文件夹)
创建文件夹的路径和名称在构造方法中给出(构造方法的参数)
返回值:
true:文件不存在,创建成功
false:文件存在,不会创建;路径不存在也返回false
注意:
1、此方法只能创建文件夹,不能创建文件
//创建最后一个路径名称的文件夹
File file = new File("G:\\Java\\API\\File\\src\\Method\\aaa");
//只创建aaa
boolean xg = file.mkdir();
//依次创建文件夹路径
File file2 = new File("G:\\Java\\API\\File\\src\\Method\\111\\222\\333\\444");
//依次查找到只要没有就一直创建,前提是盘符不能错误
boolean xg2 = file2.mkdirs();
public String[] list() 返回一个String数组,表示该File目录中的所有子文件或目录
必须遍历的是目录才可以,文件没有办法遍历,从而返回空指针异常
File file = new File("G:\\Java\\API\\File\\src\\Method");
//使用list方法获取到数组
String[] arr = file.list();
//使用增强for循环来遍历数组,并且会遍历隐藏文件夹
for (String fileName : arr) {
System.out.println(fileName);
}
public File[] listFiles() 返回一个file数组,表示该File目录中的所有的子文件或目录
遍历构造方法中给出的目录,会获取目录中所有的文件/文件夹,把文件/文件夹封装为File对象,多个File对象存储到File数组中
File file = new File("G:\\Java\\API\\File\\src\\Method");
File[] arr = file.listFiles();
System.out.println("listFiles方法的结果");
for (File f : arr) {
System.out.println(f);
}
注意:
list方法和listFiles方法遍历的是构造方法中给出的目录
如果构造方法中给出的目录不存在,就会抛出空指针异常
如果构造方法中给出的路径不是一个目录,也会抛出空指针异常
递归
递归:指在当前方法内调用自己的这种现象
递归的分类:
递归分为两种:直接递归和间接递归
直接递归称为方法自身调用自己
间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法
注意事项:
递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出
在递归中虽然有限定条件,但是递归次数不能太多,否则也会造成栈内存溢出
构造方法禁止递归
使用前提:
当调用方法的时候,方法的主题不同,每次调用方法的参数不同,可以使用递归
要是直接使用listFiles()方法,然后再遍历这个数组里面的文件/文件夹,这只能打印当前目录下的文件夹和文件夹名称,并不能将当前文件夹下面的文件/文件夹遍历出来
public static void getAllFile(File dir){
System.out.println(dir);//打印被遍历的目录名称
File[] files = dir.listFiles();
for (File f : files) {
//对遍历得到的File对象f进行判断,判断是否是文件夹
if(f.isDirectory()){
//f是一个文件夹,则继续遍历这个文件夹
//我们发现getAllFile方法就是传递文件夹,遍历文件夹的方法
//所以直接调用getAllFile方法即可:递归(自己调用自己)
getAllFile(f);
}else{
//f是一个文件,直接打印即可
System.out.println(f);
}
}
}
public static void getAllFile(File dir){
//System.out.println(dir);//打印被遍历的目录名称
File[] files = dir.listFiles();
for (File f : files) {
//对遍历得到的File对象f进行判断,判断是否是文件夹
if(f.isDirectory()){
//f是一个文件夹,则继续遍历这个文件夹
//我们发现getAllFile方法就是传递文件夹,遍历文件夹的方法
//所以直接调用getAllFile方法即可:递归(自己调用自己)
getAllFile(f);
}else{
//f是一个文件,直接打印即可
/*
c:\\abc\\abc.java
只要.java结尾的文件
1.把File对象f,转为字符串对象
*/
//String name = f.getName();//abc.java
//String path = f.getPath();//c:\\abc\\abc.java
//String s = f.toString();//c:\\abc\\abc.java
//把字符串,转换为小写
//s = s.toLowerCase();
//2.调用String类中的方法endsWith判断字符串是否是以.java结尾
//boolean b = s.endsWith(".java");
//3.如果是以.java结尾的文件,则输出
/*if(b){
System.out.println(f);
}*/
if(f.getName().toLowerCase().endsWith(".java")){
System.out.println(f);
}
}
}
}
除了使用这种笨方法,我们还可以使用文件过滤器来实现
FileFilte(文件过滤器)
在File类中有两个和ListFiles重载的方法,方法的参数传递的就是过滤器
File[] listFiles(fileFilter filter)
Java.io.FileFilter接口:用于抽象路径名(File对象)的过滤器
作用:用来过滤文件(File对象)
抽象方法:用来过滤文件的方法
boolean accept(File pathname)测试指定抽象路径名是否应该包含在某个路径名列表中。
参数:
File pathname:使用ListFiles方法遍历目录,得到的每一个文件对象
=========================分割线============================
File[] listFiles(FilenameFilter filter)
java.io.FilenameFilter接口:实现此接口的类实例可用于过滤器文件名。
作用:用于过滤文件名称
抽象方法:用来过滤文件的方法
boolean accept(File dir, String name) 测试指定文件是否应该包含在某一文件列表中。
参数:
File dir:构造方法中传递的被遍历的目录
String name:使用ListFiles方法遍历目录,获取的每一个文件/文件夹的名称
注意:
两个过滤器接口是没有实现类的,需要我们自己写实现类,重写过滤的方法accept,在方法中自己定义过滤的规则
十、接口
接口(interface)就是一种公共规范标准,是引用数据类型
定义格式
public interface new {
接口内容
}
接口名称大驼峰式
接口的抽象方法
定义格式
public interface NewAbstract {
// public abstract 返回值类型 方法名称(参数列表)
public abstract void name1();
public void name2();
abstract void name3();
void name4();
//以上都是抽象方法的定义,两个关键字都可以选择性忽略
}
注意:
1、接口中的抽象方法的修饰词是固定的两个修饰词:public abstract
2、两个修饰词可以选择性的省略掉
public abstract void name1();
public void name2();
abstract void name3();
void name4();
以上四个都是接口中的抽象方法
使用步骤:
1、接口不能直接使用,要用一个“实现类“来”实现“该接口
格式:public class 实现类名称 implements 接口名称{
}
2、接口的实现类必须全部重写接口中所有的抽象方法
3、创建实现类的对象,进行使用
注意:
如果实现类没有实现覆盖重写所有的接口抽象方法,那么这个实现类就必须是抽象类。
接口的默认方法
比如原本接口里有一个抽象方法,依靠这个抽象方法实现了两个实现类,但是这时候接口要增加抽象方法,默认方法就是在不改变原来两个实现类(实现类要重写接口的抽象方法)的方法下改变接口
格式:
public interface NewDefault {
public default void name2(){
System.out.println("111");
};//默认方法
}
public default void 新增方法名 (参数列表){方法体}
调用:
实现类名 . 默认方法名();
注意:
1、默认方法会被实现类继承下去的。
2、实现类可以覆盖重写默认方法,也可以不重写默认方法。
接口的静态方法
格式:
public interface NewStatic {
public static 返回值类型 方法名称(参数列表) {
方法体
}
}
注意:
1、不能通过接口实现类来调用接口静态方法
正确调用方法:
直接通过接口名称调用接口静态方法:接口名称 . 静态方法(参数);规避实现类
接口的私有方法
当有两个默认方法之间有多部份的重复代码,我们需要抽取出重复代码放在一个共有方法,但是这个方法不能让实现类使用,应该是私有化的,也就是说在接口里定义一个方法,只能是接口里的默认方法能调用的
定义:
私有方法有两种
1、一种是普通私有方法:解决默认方法之间重复代码问题
格式:
public interface NewPrivate {
//普通私有化
private void name() {
}
}
一种是静态私有方法
public interface NewPrivate {
//静态私有化
public static void name() {
}
}
注意:
默认方法要使用就用普通私有方法,静态方法要使用就是用静态私有方法
接口的常量
接口中也可以定义“成员变量”,但是这个成员变量必须要使用public static final三个关键字来修饰,从效果上来,这其实就是接口的常量
格式:
public static final 数据类型 数据名称 = 数据值;
注意:
1、可以省略public static final 但是就算省略了编译器还是会帮你加上去
2、常量必须赋值
3、接口中的常量名称完全使用大写字母,要是有多个下划线组成,则用_分开
访问:
不需要实现类,直接在main方法里用 接口名称 . 常量名 访问
以上内容小结
在java9+内容版本中,接口内容可以有:
1、成员变量其实就是常量,格式:
public static final 数据类型 常量名 = 数据值;
注意:
常量必须进行赋值,而且一旦赋值不能改变
常量名完全大写,用下划线进行分隔。
2、接口中最重要的就是抽象方法,格式:
public abstract 返回值类型 方法名称(参数列表);
注意:
实现类必须覆盖重写接口所有的抽象方法,除非实现类是抽象类
3、从Java8开始,接口里允许定义默认方法,格式:
public default 返回值类型 方法名称(参数列表){方法体}
注意:
默认方法也可以被重写
4、从Java8开始,接口里允许定义静态方法,格式:
public static 返回值类型 方法名称(参数列表){方法体}
注意:
应该通过接口 名称进行调用,不能通过实现类对象调用接口静态方法
5、从Java9开始,接口里允许定义私有方法,格式:
普通私有方法 private 返回值类型 方法名(参数列表){方法体}
静态私有方法 private static 返回值类型 方法名(参数列表){方法体}
注意:
private的方法只有接口自己才能调用,不能被实现类或别人使用
使用接口的时候要注意:
1、接口是没有静态代码块或者构造方法的
2、一个类的直接父类是唯一的,但是一个类可以同时实现多个接口
格式:
public class 实现类名称implements 接口1,接口2{
覆盖重写两个接口的所有抽象方法
}
3、如果实现类所实现的多个接口当中,存在重复的抽象方法,那么只需要覆盖重写一次即可
4、如果实现类没有覆盖重写所有的接口方法,那么这个实现类必须是抽象类
5、是如果实现类所实现的多个接口当中,有重复的默认方法,那么实现类一定要对有冲突的默认方法进行覆盖重写。
6、要是父类的方法和接口的默认方法有冲突,则先使用父类的方法
接口之间的多继承
1、类与类之间是单继承的,直接父类只有一个。
2、类与接口之间是多实现的。一个类可以实现多个接口。
3、接口与接口之间是多继承的
注意:
1、多个父接口当中的抽象方法如果重复,没问题。
2、多个父接口当中的默认方法如果重复,那么子接口必须进行默认方法的覆盖重写,并且要带着default !!!
十一、异常
异常(Throwable)
异常:指的是程序在执行过程中,出现的非正常情况,最终会导致JVM非正常停止(指的并不是语法错误)
在Java等面向对象的编程语言中,异常本身就是一个类,产生那个一场就是创建异常对象并抛出了一个异常对象。java处理异常的方式是中断处理
异常体系:
异常机制是帮助我们找到程序中的问题,异常的最高父类是Throwable,它有两个子类:Error与Exception,其中Error工程师不能处理,只能尽量避免;而通常所说的异常是指Exception
异常分类:
Exception:编译期异常,进行编译(写代码)java程序出现问题
runtimeException:运行期异常,java程序运行过程中出现的问题
异常就相当于程序得了一个小毛病,把异常处理掉,程序可以 继续执行
Error:错误
错误就相当于程序必须修改源代码,程序才能继续运行
异常产生的解析
分析异常是如何产生的,如何处理异常
当JVM检测到程序出现异常,会进行以下处理
第一步:
1、JVM会根据异常产生的原因创建一个异常对象,这个异常对象包含了异常产生的(内容,原因,位置)
2、在getElement方法中,没有异常的处理逻辑(try…catch),那么JVM就会把异常对象抛出给方法的调用者main方法来处理这个异常
第二步:
3、main方法接收到这个异常对象,main方法也没有异常的处理逻辑,继续把对象抛出给main方法的调用者JVM处理
第三步:
4、把异常对象(内容,原因,位置)以红色的字体展示在控制台上
5、JVM终止当前正在执行的Java程序
异常的处理:
异常处理的五个关键字:try , catch , finally , throw , throws
抛出异常throw
作用:可以使用throw关键字在指定的方法中抛出指定的异常
使用格式:throw new xxxException(“异常产生的原因”)
注意:
1、throw关键字必须写在方法的内部
2、throw关键字后边new的对象必须是Exception或者Exception的子类对象
3、throw关键字抛出指定的异常对象,我们就必须要处理这个异常对象
throw关键字后边创建的是RuntimeException(运行期异常)或者RuntimeException的子类对象,我们可以不处理,默认交给JVM处理(打印异常对象,终止程序)
throw关键字后边创建的是编译异常(写代码的时候报错),我们就必须处理这个异常要么throws,要么try…catch。
//加一道判断,要是传过来的数组是空的,则返回数组是空的
if (arr == null) {
throw new NullPointerException("数组是空的");
}
NullPointerException是RuntimeException(运行期异常),直接抛出,终止程序
//再加一道判断,要是索引值越界则告知
if (index < 0 ||index > arr.length) {
throw new ArrayIndexOutOfBoundsException("索引值越界");
}
运行期异常:都是无需处理的
Objects的非空判断:
public static void method(Object obj) {
//对传递过来的参数进行合法性判断,判断是否为null
if (obj == null) {
throw new NullPointerException("传递的对象的值是null");
}
}
这是手动书写的,但是Objects有一个自带的方法public static <T> requireNonNull(T obj)查看引用对象是不是null;
public static void method_b(Object obj) {
// 使用Objects自带的方法来判断合法性
Objects.requireNonNull(obj);
}
throws声明异常
异常处理的第一种方式,交给别人处理
作用:
当方法内部抛出异常对象的时候,那么哦我们就必须处理这个异常对象
可以使用throws关键字处理异常对象,会把异常对象声明抛出给方法的调用者处理(自己不处理,给别人处理),最后交给JVM处理(终止程序)
声明异常格式:
修饰符 返回值类型 方法名(参数)throwsAAAException,BBBException{
throw new AAAException(“异常类型1”)
throw new BBBException(“异常类型2”)
}
通常和throw搭配使用,throw抛出一个异常,throws就声明一个异常
注意:
1、throws关键字必须写在方法声明处;
2、throws关键字后边声明的异常必须是Exception或者是Exception的子类
3、方法内部如果抛出了多个异常对象,那么throws后边必须也声明多个异常(如果抛出的多个异常对象有子父类关系,那么直接声明父类异常即可)
4、调用了一个生命抛出异常的方法,我们就必须得处理声明的异常(要么继续使用throws声明抛出,交给方法的调用者处理,最终交给JVM)(要么try…catch自己处理异常)
缺陷:
要是异常声明完之后,后续还有代码,则无法处理后续代码
public static void readFile(String fileName) throws IOException {
if (!fileName.equals("c:\\a.txt")) {
throw new FileNotFoundException("传递的文件路径不是c:\\a.txt");
}
//如果传递的路径不是.txt结尾的,则抛出IO异常
if (fileName.endsWith(".txt")) {
throw new IOException("文件后缀名错误");
}
}
捕获异常try…catch
异常处理的第二种方式,自己处理异常
格式:
try{
可能产生异常的代码
}catch(定义一个异常的变量,用来接收try中抛出的异常对象){
异常处理逻辑,产生异常对象之后,怎么处理对象
一般在工作中,会把异常的信息记录到一个日志中
}catch(){
}
注意:
1、try中可能会抛出多个异常对象,那么就可以使用多个catch来处理这些异常对象
2、如果try中产生了异常,那么就不会执行catch中的异常处理逻辑,执行完毕catch中的处理逻辑,继续执行try…catch之后的代码(如果try中没有产生异常,那么就不会执行catch中异常的处理逻辑,执行完try中的代码。)
throwable异常处理类
只要是throwable和throwable的子类都可以直接调用
里面定义了三个异常处理的方法:
String getMessage() 返回此throwable的简短描述
String toString() 返回此throwable的详细消息字符串。
void printStackTrace() JVM打印异常对象,默认此方法,打印的异常信息最全面
System.out.println(e.getMessage()); //返回此throwable的简短描述(文件后缀名错误)
System.out.println(e.toString()); //返回此throwable的详细消息字符串(java.io.IOException: 文件后缀名错误)
e.printStackTrace();
/**
* java.io.IOException: 文件后缀名错误
at app.demo03throwable.readFile(demo03throwable.java:27)
at app.demo03throwable.main(demo03throwable.java:13)
*/
finally代码块
和try…catch代码块搭配使用
格式:
try{
可能产生异常的代码
}catch(定义一个异常的变量,用来接收try中抛出的异常对象){
异常处理逻辑,产生异常对象之后,怎么处理对象
一般在工作中,会把异常的信息记录到一个日志中
}catch(){
}finally{
无论代码是否异常都会执行
}
try {
//可能会出现异常的代码
readFile("c:\\a.tx");
} catch (IOException e) {//IOException是指异常出现的情况
//出现异常的处理逻辑
e.printStackTrace();
} finally {
//无论是否出现异常都会执行
System.out.println("资源释放");
}
注意:
finally不能单独使用,必须和try…catch一起使用
finally一般用于资源释放(回收),无论程序是否出现异常,最后都要资源释放(IO)
异常的注意事项
多个异常使用捕获又该如何处理:
1、多个异常分别处理:分别捕获 ,分别处理
2、多个异常一次捕获,多次处理:将可能出现异常的所有代码都写在try里面,一次捕获,然后再catch里多次列出异常的情况分别处理
一个try多个catch注意事项:catch里边定义的异常变量,如果有子父类关系,那么子类的异常变量必须写在上边,否则就会报错
3、多个异常一次捕获,一次处理:catch直接使用Exception处理所有异常
运行期异常不需要处理,可以直接交给虚拟机(JVM)处理;
如果finally有return语句,永远返回finally中的结果 ,要避免;
子父类异常:
如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。
父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出
注意:父类异常是什么样,子类异常就什么样
自定义异常
java提供的异常类,不够我们使用,需要自己定义一些异常类
格式:
public class XXXException extends Exception(RuntimeException){
添加一个空参数的构造方法
添加一个带异常信息的构造方法
}
注意:
1、自定义异常类一般都是以Exception结尾,说明该类是一个异常类
2、自定义异常类,必须继承Exception或者RuntimeException
继承Exception:那么自定义的异常类就是一个编译期异常,如果方法内部抛出了编译期异常,就必须处理这个异常,要么throws,要么try…catch
继承RuntimeException:那么自定义的异常类就是一个运行期异常,无需处理,交给虚拟机处理(中断处理)
十二、多线程
并发和并行
并发:指两个或多个事件在同一时间段内发生(交替发生)
并行:指两个或多个事件在同一时刻发生(同时发生)
线程与进程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程,进程也是程序的依次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程至少有一个线程。一个进程中是可以有多个线程的,一个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
线程调度
分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的就是抢占式调度
创建线程类
Java默认的是单线程,也就是从上往下依次执行,但是要是再执行过程中有一点错误,当执行到有错误的位置的时候,就直接进行报错,错误后面的代码不会执行;而多线程就避免了这种情况
创建Thread的子类
java,lang,Thread类:描述线程的类,我们想要实现多线程程序,就必须继承Thread类
实现步骤:
1、创建一个Thread类的子类
2、在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么)
3、创建Thread类的子类对象
4、调用Thread类中的方法start方法,开启新的线程,执行run方法
void start()
使该线程开始执行;Java虚拟机调用该线程的run方法。
结果是两个线程并发地运行,当前线程(main线程)和另一个线程(创建的新线程,执行其run方法)。
多次启动一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动。
1、使用Thread类中的方法getName();
String getName() 返回该线程的名称
2、可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
static Thread currentThread() 返回对当前正在执行的线程对象的引用
1、使用Thread类中的方法setName(名字);
void setName(String name) 改变线程名称,使之与参数name相同
2、创建一个带参数的构造方法,参数传递线程名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(thread)给子线程起一个名字。
public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂停停止执行)。
毫秒数结束之后,线程继续执行
实现Runnable接口
Java.lang.Runable:
Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run的无参数方法
Java.lang.Thread:
Thread(Runnable target) 分配新的Thread对象
Thread(Runnable target,String name) 分配新的Thread对象
实现步骤:
1、创建一个Runnable接口的实现类
2、在实现类中重写Runnable接口的run方法,设置线程任务
3、创建一个Runnable接口的实现类对象
4、创建Thread类对象,构造方法中传递Runnable接口的实现类对象
5、调用Thread类中的start方法,开启新的线程执行run方法
Runnable和Thread的差距
使用Runnable的好处:
1、避免了单继承的局限性
一个类只能继承一个类,类继承Thread类就不能继承其他的类
实现了Runnable接口,还可以继承其他的类,实现其他的接口
2、增强了程序的扩展性,降低了程序的耦合性(又称:解耦)
实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
实现类中,重写了run方法:用来设置线程任务
创建Thread类对象,调用start方法:来开启新线程
工作中最好使用Runnable来进行工作
匿名内部类创建线程
作用:简化代码
把子类继承父类,重写父类的方法,创建子类对象合一步完成
把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字
格式:
new 父类/接口(){
重复父类/接口中的方法
}
线程安全
当我们使用多个线程访问同一资源的时候,且多个线程对资源中有写的操作,就容易出现线程安全问题。
线程同步
同步线程有三种方法:
1、同步代码块
2、同步方法
3、锁机制
同步代码块
格式:
synchronizde(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
1、通过代码块中的锁对象,可以使用任意对象
2、但是必须保证多个线程使用的锁对象是同一个
3、锁对象作用:
把同步代码块锁住,只让一个线程在同步代码块中执行
同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着
步骤:
1、把访问了共享数据的代码抽取出来,放到一个方法中
2、在方法上添加synchronized修饰符
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
Lock锁(锁机制)
java.util.concurrent.locks.Lock接口,它提供了比使用synchronized方法和语句更广泛的锁定操作。
lock接口中的方法:
void lock() 获取锁
void unlock() 释放锁
使用步骤:
1、在成员位置创建一个Reentrantlock对象
2、在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
3、在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
线程状态
线程概述
线程状态 | 导致状态发生条件 |
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocke状态;当该线程持有锁时,该线程将变成Runnable状态 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒 |
TimedWaiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态 将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |
睡眠方法:
Object.wait() 使一个线程沉睡
Object.wait(long m) 要是wait再m毫秒之内没有被唤醒则自动苏醒
唤醒方法:
Object.notify() 唤醒单个线程(如果有多个等待线程,随即唤醒一个)
Object.notifyAll() 唤醒所有沉睡的线程
等待唤醒机制(线程通信)
等待与唤醒机制,也称之为线程之间的通信
重点:有效的利用资源(生产一个消费一个,在生产一个在消费一个)
等待唤醒中的方法
1、wait:线程不在活动,不再参与调度,进入waitset中,因此不会浪费cpu资源,也不会去竞争锁,此时的线程状态即使WAITING,他还要等着别的线程执行一个特别的动作,也就是notify,在这个对象上等待的线程从waitset中释放出来,重新进入到调度队列(ready queue)中
2、notify:则选取所通知对象的waitset中的一个线程释放(唤醒),要是有多个处在WAITING状态的线程,则随机唤醒一个
3、notifyAll:释放所通知对象上的全部线程
注意:要是线程被notify唤醒了,如果能获取到锁,则会从WAITING状态变成Runnable状态,要是没有获取到锁,则从WAITING状态变成Blocked(锁阻塞)状态
wait和notify调度的细节
1、wait方法和notify方法必须由同一个锁对象调用,因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
2、wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的
3、wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法
线程池
Java是JDK1.5之后提供的
其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。
三个好处:
1、降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2、提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
3、提高线程的可管理性
Java里面线程池的顶级接口是Java.util.concurrent.Executor,但严格意义上讲Executor并不是一个线程池,而是一个执行线程的工具。真正的线程程池接口是Java.util.concurent.ExecutorService。
线程池的使用
Java.util.concurrent.Executors:线程池的工厂类,用来生产线程池
Executors类中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程的线程池。
参数:
int nThreads:创建线程池中包含的线程数量
返回值:
ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)
java.util.concurrent.ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务
submit(Runnable task) 提交一个 Runnable 任务用于执行
关闭/销毁线程池的方法
void shutdown()
线程池的使用步骤:
1、使用线程池的工厂类Executors里面提供了静态方法newFixedThreadPool生产一个指定线程数量的线程池
2、创建一个类,实现Runnable接口,重写run方法,设置线程任务
3、调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
4、调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
Lambda表达式
Lambda强调的是做什么,而不是以什么形式去做(函数式编程思想)
面向对象的思想:
做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情
函数式编程思想:
只要能获取到结果,谁去做的,怎么做的并不重要,重视的是结果,并不在乎过程。
要是想要实现多线程,我们并不是想要去创建一个实现类而去创建一个实现类,是因为语法的问题,我们不得不去创建一个实现类,即使是使用匿名内部类我们也会有很多冗余的代码,所以在JDK1.8之后,有一个新特性Lambda,来帮助我们减少冗余的代码
//使用匿名内部类实现多线程
new Thread(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程创建了");
}
}).start();
//使用Lambda表达式来实现多线程
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "线程创建了");
}).start();
匿名内部类的好处与弊端:一方面,匿名内部类可以帮我们省去实现类的定义;另一方面,匿名内部类的语法,确实太复杂了
Lambda标准格式
一些参数,一个箭头,一段代码
(参数类型 参数名称) ->{一些重写方法的代码(方法体)};
Lambda省略格式
1、(参数列表):括号中参数列表的数据类型,可以省略不屑
2、(参数列表):括号中的参数如果只有一个,那么类型和()都可以省略
3、{一些代码}:如果{}中的代码只有一行,无论是否有返回值,都可以省略({},return,分号)
注意:要省略{},return,分号必须一起省略
使用前提:
1、使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法
2、使用Lambda必须具有上下文推断:也就是方法的参数或局部变量类型必须味Lambda对应的接口类型,才能使用Lambda作为该接口的实例
有且仅有一个抽象方法的接口,称为“函数式接口”
十三、IO流
我们将数据的传输,看作是一种数据的流动,按照流动方向,以内存为基准,分为输出input和输出output,即流向内存的是输入流,流出内存的是输出流
I:input输入(读取)
O:output输出(写入)
流:数据(字符,字节)一个字符 = 两个字节,一个字节 = 8个二进制
输入流 | 输出流 | |
字节流 | 字节输入流 InputStream | 字节输出流 OutputStream |
字符流 | 字符输入流 Reader | 字符输出流 Writer |
字节流
一切文件数据(文本、图片、视频)在存储时,都是以二进制数字的形式保存,都是一个一个的字节,
字节输出流OutputStream
Java.io.OutputStream:此抽象类是表示输出字节流的所有类的超类(父类)
子类:
定义了一些子类共性的成员方法:
public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
public abstract void write(int b) :将指定的字节输出流。
注意:使用write方法输入0~127之间的数字的时候,会按照ASCII码来转换成对应的字符。(比如:97àa)
作用:把内存中的数据写入到硬盘的文件中
构造方法:
FileOutputStream(String name)创建一个向具有指定名称的文件中写入数据的输出文件流。
FileOutputStream(File file) 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。
参数:写入数据的目的
String name:目的地是一个文件的路径
File file:目的地是一个文件
构造方法的作用:
1.创建一个FileOutputStream对象
2.会根据构造方法中传递的文件/文件路径,创建一个空的文件
3.会把FileOutputStream对象指向创建好的文件
写入数据的原理(内存-->硬盘)
java程序-->JVM(java虚拟机)-->OS(操作系统)-->OS调用写数据的方法-->把数据写入到文件中
字节输出流的使用步骤(重点):
1.创建一个FileOutputStream对象,构造方法中传递写入数据的目的地
2.调用FileOutputStream对象中的方法write,把数据写入到文件中
3.释放资源(流使用会占用一定的内存,使用完毕要把内存清空,提供程序的效率)
单个字节的输出:
//1.创建一个FileOutputStream对象,构造方法中传递写入数据的目的地
FileOutputStream fos = new FileOutputStream("09_IOAndProperties\\a.txt");
//2.调用FileOutputStream对象中的方法write,把数据写入到文件中
//public abstract void write(int b) :将指定的字节输出流。
fos.write(97);
//3.释放资源(流使用会占用一定的内存,使用完毕要把内存清空,提供程序的效率)
fos.close();
多个字节的输出:
// 1、创建方法
FileOutputStream fop = new FileOutputStream("src\\OutputSteam\\demo02.txt");
/*
public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
一次写多个字节:
如果写的第一个字节是正数(0-127),那么显示的时候会查询ASCII表
如果写的第一个字节是负数,那第一个字节会和第二个字节,两个字节组成一个中文显示,查询系统默认码表(GBK)
*/
byte[] byteArr = {45 , 46 , 47};
byte[] byteArr2 = {-45 , -46 , -47 , -48};
//2、放入数组
fop.write(byteArr);
fop.write(byteArr2);
//3、释放资源
fop.close();
多个字节指定输出:
// 1、创建方法
FileOutputStream fop = new FileOutputStream("src\\OutputSteam\\demo03.txt");
/*
public void write(byte[] b, int off, int len) :把字节数组的一部分写入到文件中
int off:数组的开始索引
int len:写几个字节
*/
byte[] byteArr = {45 , 46 , 47};
//2、放入数组
//从索引值为1开始写,写入2个字节
fop.write(byteArr, 1 , 2);
//3、释放资源
fop.close();
注意:String包下面有一个方法getBytes();这个就是将字符串转化成字符数组
// 1、创建方法
FileOutputStream fop = new FileOutputStream("src\\OutputSteam\\demo02.txt");
//Java.lang.String包下面有个方法getBytes(),将字符串以字节的方法返回
byte[] byteArr = "你好".getBytes();
//2、放入数组
fop.write(byteArr);
//3、释放资源
fop.close();
要实现追加/续写:要使用两个构造方法
FileOutputStream(String name, boolean append)创建一个向具有指定name的文件中写入数据的输出文件流。
FileOutputStream(File file, boolean append)创建一个向指定File对象表示的文件中写入数据的文件输出流。
参数:
String name,File file:写入数据的目的地
Boolean append:追加写开关
true:创建对象不会覆盖源文件,继续在文件的末尾追加写数据。
flase:创建一个新文件,覆盖源文件。
FileOutputStream fos = new FileOutputStream("src\\OutputStream\\xuXieHuanHang.txt", true);
byte[] byteArr = "你好".getBytes();
fos.write(byteArr);
fos.close();
这就是一直在这个文件后面追加写你好,并不会每次都是重新书写,要是不写append的话,则默认是false。
写换行:添加写换行符号
Windows:\r\n
Linux:\n
Mac:\r
FileOutputStream fos = new FileOutputStream("src\\OutputStream\\xuXieHuanHang.txt", true);
for (int i = 0; i < 10; i++) {
fos.write("你好".getBytes());
//写换行符号
fos.write("\r\n".getBytes());
}
fos.close();
字节输入流InputStream
java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。
所有子类:
它定义了字节输入流的基本共性功能方法。
public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
public abstract int read() : 读取文件中的一个字节并返回,读取到文件的末尾返回-1。
public int read(byte[] b) : 从输入流中读取一些字节数,并将它们存储到字节数组b中。
把硬盘文件中的数据,读取到内存中使用
构造方法:
FileInputStream(String name)
FileInputStream(File file)
参数:
String name:文件的路径
File file:文件
构造方法的作用:
- 会创建一个FileInputStream对象
- 会把FileInputStream对象指向构造方法中要读取的文件
读取数据的原理(硬盘-->内存)
java程序-->JVM-->OS-->OS读取数据的方法-->读取文件
字节输入流的使用步骤(重点):
1.创建FileInputStream对象,构造方法中绑定要读取的数据源
2.使用FileInputStream对象中的方法read,读取文件
3.释放资源
一个字节输入:
//1、使用构造方法
FileInputStream fis = new FileInputStream("src\\OutputStream\\demo01.txt");
//2、使用int read()方法读取字节
//read()方法读取的是下一个字节,返回字节代表的ASCII码,读取到文件的末尾则返回-1
int b = fis.read();
System.out.println(b);//第一个a,对应97
b = fis.read();
System.out.println(b);//b,98
b = fis.read();
System.out.println(b);//c,99
b = fis.read();
System.out.println(b);//因为文档里面只有abc,所以最后就会显示是-1
//3、释放资源
fis.close();
以上读取文件是一个重复的过程,可以使用循环优化
不知道文件中有多少字节,使用while循环
int len = 0;//记录读取到的字节
while ((len = fis.read()) != -1) {
System.out.println((char)len);//将ASCII码转换成字符打印出来
}
多个字节输入:
int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组b中。
数组b起到缓冲作用,存储每次读取到的多个字节,数组的长度一般定义为1024(1KB)或者1024的整数倍
返回值int是每次读取的有效字节个数
//构造方法
FileInputStream fis = new FileInputStream("src\\OutputStream\\demo01.txt");
//创建一个数组用来存储字节,数组定义多大就是一次读取多少位字节
byte[] arr = new byte[10];//一次读取十位字节
//创建一个变量用来接收返回值(一次读取了多少有效字节)
int len = 0;
//使用while循环来便利
while ((len = fis.read(arr)) != -1) {
//String(byte[] bytes, int offset, int length) 把字节数组的一部分转换为字符串 offset:数组的开始索引 length:转换的字节个数
System.out.println(new String(arr,0,len));
}
//释放资源
fis.close();
文件复制
就是一读一写
文件复制的步骤:
1.创建一个字节输入流对象,构造方法中绑定要读取的数据源
2.创建一个字节输出流对象,构造方法中绑定要写入的目的地
3.使用字节输入流对象中的方法read读取文件
4.使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
5.释放资源
注意:释放资源的时候要先释放输出流的资源,因为可能输入流结束后输出流还没有结束,但是输出流结束了输入流一定结束了
字符流
当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符的时候,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供了一些字符流类,以字符为单位读写数据,专门用于处理文本文件
字符输入流Reader
Java.io.Reader是字符输入流最顶层的父类,定义了一些共性的成员方法,是一个抽象类。
共性的成员方法:
int read() 读取单个字符并返回
int read(char[] cbuf) 一次读取多个字符,将字符存入数组
void close() 关闭并释放与之关联的所有资源
所有子类:
java.io.FileReader extends InputStreamReader extends Reader;
将硬盘文件中的数据以字符的方式读取到内存中
构造方法:
FileReader(String fileName)
FileReader(File file)
参数:读取文件的来源
String fileName:文件的路径
File file:一个文件
FileReader构造方法的作用:
1、创建一个FileReader对象
2、会把FileReader对象指向要读取的文件
字符流的使用步骤:
1、创建FileReader对象,构造方法中绑定要读取的数据源
2、使用FileReader对象中的方法read读取文件
3、释放资源
单个字符:
// 创建对象
FileReader fr = new FileReader("src\\OutputStream\\xuXieHuanHang.txt");
// 创建接受数
int len = 0;
// 使用while循环
while ((len = fr.read()) != -1) {
System.out.println(len);
}
// 释放资源
fr.close();
多个字符:
FileReader fr = new FileReader("src\\OutputStream\\xuXieHuanHang.txt");
// 有效字符位
int len = 0;
//一次读取两位字符
char[] arr = new char[2];
while ((len = fr.read(arr)) != -1) {
// String(char[] value, int offset, int count)
// 把字符数组的一部分转换为字符串 offset数组的开始索引 count转换的个数
System.out.println(new String(arr, 0, len));
}
fr.close();
字符输出流Writer
java.io.Writer:字符输出流,是所有字符输出流的最顶层的父类,是一个抽象类
共性的成员方法:
void write(int c) 写入单个字符。
void write(char[] cbuf) 写入字符数组。
abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
void write(String str) 写入字符串。
void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
void flush() 刷新该流的缓冲。
void close() 关闭此流,但要先刷新它。
java.io.FileWriter extends OutputStreamWriter extends Writer
把内存中的字符数据写入到文件中
构造方法:
FileWriter(String fileName)
FileWriter (File file)
参数:读取文件的来源
String fileName:文件的路径
File file:一个文件
构造方法的作用:
1、创建一个FileWriter对象
2、会根据构造方法中传递的文件/文件的路径创建文件
3、会把FileWriter对象指向创建好的文件
使用步骤(重点):
1、创建FileWriter对象,构造方法中绑定要写入数据的目的地
2、使用FileWriter中的方法write,把数据写入到内存缓冲区中(字符转换为字节的过程)
3、使用FileWriter中的方法flush,把内存缓冲区中的数据,刷新到文件中
4、释放资源(会先把内存缓冲区中的数据刷新到文件中)
基础使用:
//1.创建FileWriter对象,构造方法中绑定要写入数据的目的地
FileWriter fw = new FileWriter("09_IOAndProperties\\d.txt");
//2.使用FileWriter中的方法write,把数据写入到内存缓冲区中(字符转换为字节的过程)
//void write(int c) 写入单个字符。
fw.write(97);
//3.使用FileWriter中的方法flush,把内存缓冲区中的数据,刷新到文件中
fw.flush();
//4.释放资源(会先把内存缓冲区中的数据刷新到文件中)
fw.close();
注意:flush和close都有刷新缓冲区的功能,但是有一点差距
flush方法和close方法的区别
- flush :刷新缓冲区,流对象可以继续使用。
- close: 先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
其他使用方法:
FileWriter fw = new FileWriter("src\\Writer\\demo02.txt");
char[] charArr = {'l','j','y'};
//void write(char[] cbuf)写入字符数组
fw.write(charArr);
fw.flush();// 刷新缓冲区
// abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
fw.write(charArr, 1, 2);
fw.flush();// 刷新缓冲区
// void write(String str)写入字符串。
fw.write("Lvandjy");
fw.flush();// 刷新缓冲区
// void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
fw.write("Lvandjy", 0, 2);
fw.flush();// 刷新缓冲区
// 释放资源
fw.close();
与字节输出流的续写换行一样:
续写和换行
续写,追加写:使用两个参数的构造方法
FileWriter(String fileName, boolean append)
FileWriter(File file, boolean append)
参数:
String fileName,File file:写入数据的目的地
boolean append:续写开关 true:不会创建新的文件覆盖源文件,可以续写; false:创建新的文件覆盖源文件
换行:换行符号
windows:\r\n
linux:\n
mac:\r
FileWriter fw = new FileWriter("09_IOAndProperties\\g.txt",true);
for (int i = 0; i <10 ; i++) {
fw.write("HelloWorld"+i+"\r\n");
}
fw.close();
IO流异常处理:
JDK1.7之前:(重要)
// 提高变量fw的作用域,让finally可以使用
// 变量在定义的时候,可以没有值,但是使用的时候必须有值
// fw = new FileWriter("09_IOAndProperties\\g.txt",true); 执行失败,fw没有值,fw.close会报错
FileWriter fw = null;
try {
// 可能会产出异常的代码
fw = new FileWriter("w:\\09_IOAndProperties\\g.txt", true);
for (int i = 0; i < 10; i++) {
fw.write("HelloWorld" + i + "\r\n");
}
} catch (IOException e) {
// 异常的处理逻辑
System.out.println(e);
} finally {
// 一定会执行的代码
// 创建对象失败了,fw的默认值就是null,null是不能调用方法的,会抛出NullPointerException,需要增加一个判断,不是null在把资源释放
if (fw != null) {
try {
// fw.close方法声明抛出了IOException异常对象,所以我们就的处理这个异常对象,要么throws,要么try catch
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
JDK7新特性:(了解)
JDK7的新特性
在try的后边可以增加一个(),在括号中可以定义流对象
那么这个流对象的作用域就在try中有效
try中的代码执行完毕,会自动把流对象释放,不用写finally
格式:
try(定义流对象;定义流对象....){
可能会产出异常的代码
}catch(异常类变量 变量名){
异常的处理逻辑
}
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("c:\\1.jpg");
FileOutputStream fos = new FileOutputStream("d:\\1.jpg");) {
// 可能会产出异常的代码
int len = 0;
while ((len = fis.read()) != -1) {
fos.write(len);
}
} catch (IOException e) {
// 异常的处理逻辑
System.out.println(e);
}
}
JDK9新特性:(不如JDK1.7)
JDK9新特性
try的前边可以定义流对象
在try后边的()中可以直接引入流对象的名称(变量名)
在try代码执行完毕之后,流对象也可以释放掉,不用写finally
格式:
A a = new A();
B b = new B();
try(a,b){
可能会产出异常的代码
}catch(异常类变量 变量名){
异常的处理逻辑
}
//1.创建一个字节输入流对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("c:\\1.jpg");
//2.创建一个字节输出流对象,构造方法中绑定要写入的目的地
FileOutputStream fos = new FileOutputStream("d:\\1.jpg");
try(fis;fos){
//一次读取一个字节写入一个字节的方式
//3.使用字节输入流对象中的方法read读取文件
int len = 0;
while((len = fis.read())!=-1){
//4.使用字节输出流中的方法write,把读取到的字节写入到目的地的文件中
fos.write(len);
}
}catch (IOException e){
System.out.println(e);
}
属性集:
属性集是指的一个集合(Properties)
java.util.Properties集合 extends Hashtable<k,v> implements Map<k,v>
Properties 类表示了一个持久的属性集。Properties 可保存在流中或从流中加载。
Properties集合是一个唯一和IO流相结合的集合
可以使用Properties集合中的方法store,把集合中的临时数据,持久化写入到硬盘中存储
可以使用Properties集合中的方法load,把硬盘中保存的文件(键值对),读取到集合中使用
属性列表中每个键及其对应值都是一个字符串。
Properties集合是一个双列集合,key和value默认都是字符串
Properties集合有一些操作字符串的特有方法
Object setProperty(String key, String value) 调用 Hashtable 的方法 put。
String getProperty(String key) 通过key找到value值,此方法相当于Map集合中的get(key)方法
Set<String> stringPropertyNames() 返回此属性列表中的键集,其中该键及其对应值是字符串,此方法相当于Map集合中的keySet(获取map中所有的key保存在set里面)方法
基础使用方法:
//创建Properties对象
Properties prop = new Properties();
// 往里面添加键值对(properties默认键值对都是字符串)
prop.setProperty("ljy", "18");
prop.setProperty("zql", "20");
prop.setProperty("sh", "19");
prop.setProperty("cp", "18");
// 使用stringPropertyNames方法取出key
Set<String> key = prop.stringPropertyNames();
// 使用增强for循环通过key来找到value
for (String string : key) {
String h = prop.getProperty(string);
System.out.println(string + "的年龄是" + h);
}
store方法:(输出)
可以使用Properties集合中的方法store,把集合中的临时数据,持久化写入到硬盘中存储
void store(OutputStream out, String comments)
void store(Writer writer, String comments)
参数:
OutputStream out:字节输出流,不能写入中文
Writer writer:字符输出流,可以写中文
String comments:注释,用来解释说明保存的文件是做什么用的
不能使用中文,会产生乱码,默认是Unicode编码
一般使用""空字符串
使用步骤:
1.创建Properties集合对象,添加数据
2.创建字节输出流/字符输出流对象,构造方法中绑定要输出的目的地
3.使用Properties集合中的方法store,把集合中的临时数据,持久化写入到硬盘中存储
4.释放资源
//1.创建Properties集合对象,添加数据
Properties prop = new Properties();
prop.setProperty("ljy", "18");
prop.setProperty("zql", "20");
prop.setProperty("sh", "19");
prop.setProperty("cp", "18");
//2.创建字节输出流/字符输出流对象,构造方法中绑定要输出的目的地
FileWriter fw = new FileWriter("src\\Properties\\show02.txt");
//3.使用Properties集合中的方法store,把集合中的临时数据,持久化写入到硬盘中存储
prop.store(fw,"save data");
//4.释放资源
fw.close();
load方法:(输入)
可以使用Properties集合中的方法load,把硬盘中保存的文件(键值对),读取到集合中使用
void load(InputStream inStream)
void load(Reader reader)
参数:
InputStream inStream:字节输入流,不能读取含有中文的键值对
Reader reader:字符输入流,能读取含有中文的键值对
使用步骤:
1.创建Properties集合对象
2.使用Properties集合对象中的方法load读取保存键值对的文件
3.遍历Properties集合
注意:
1.存储键值对的文件中,键与值默认的连接符号可以使用=,空格(其他符号)
2.存储键值对的文件中,可以使用#进行注释,被注释的键值对不会再被读取
3.存储键值对的文件中,键与值默认都是字符串,不用再加引号
// 1.创建Properties集合对象
Properties prop = new Properties();
// 2.使用Properties集合对象中的方法load读取保存键值对的文件
prop.load(new FileReader("src\\Properties\\show02.txt"));
// 3.遍历Properties集合
Set<String> key = prop.stringPropertyNames();
for (String string : key) {
String value = prop.getProperty(string);
System.out.println(string + value);
}
缓冲流
字节流和字符流在输出/输入过程中,是一个一个的传输,输出一个先调用JVM在调用系统方法输出一个,输出第二个又要再次执行此操作,效率十分低下,所以这时候缓冲流就出现了,缓冲流的本质是一个数组,将要输出/输入的字符/字节装在一个数组里,然后一次性传输,减少IO读写步骤,所以缓冲流也叫高效流,是四个基础流的增强。
字节缓冲流
java.io.BufferedOutputStream extends OutputStream
继承自父类的共性成员方法:
- public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
- public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
- public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
- public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
- public abstract void write(int b) :将指定的字节输出流。
构造方法:
BufferedOutputStream(OutputStream out) 创建一个新的缓冲输出流,以将数据写入指定的底层输出流。
BufferedOutputStream(OutputStream out, int size) 创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流。
参数:
OutputStream out:字节输出流
我们可以传递FileOutputStream,缓冲流会给FileOutputStream增加一个缓冲区,提高FileOutputStream的写入效率
int size:指定缓冲流内部缓冲区的大小,不指定默认
使用步骤(重点)
1.创建FileOutputStream对象,构造方法中绑定要输出的目的地
2.创建BufferedOutputStream对象,构造方法中传递FileOutputStream对象对象,提高FileOutputStream对象效率
3.使用BufferedOutputStream对象中的方法write,把数据写入到内部缓冲区中
4.使用BufferedOutputStream对象中的方法flush,把内部缓冲区中的数据,刷新到文件中
5.释放资源(会先调用flush方法刷新数据,第4部可以省略)
FileOutputStream fos = new FileOutputStream("src\\BufferedOutputStream\\demo.txt");
// 2.创建BufferedOutputStream对象,构造方法中传递FileOutputStream对象对象,提高FileOutputStream对象效率
BufferedOutputStream bos = new BufferedOutputStream(fos);
// 3.使用BufferedOutputStream对象中的方法write,把数据写入到内部缓冲区中
bos.write("使用字节缓冲区输入".getBytes());
// 4.刷新缓冲区
bos.flush();
// 5.释放资源
bos.close();
java.io.BufferedInputStream extends InputStream
BufferedInputStream:字节缓冲输入流
继承自父类的成员方法:
int read()从输入流中读取数据的下一个字节。
int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
void close() 关闭此输入流并释放与该流关联的所有系统资源。
构造方法:
BufferedInputStream(InputStream in) 创建一个 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。
BufferedInputStream(InputStream in, int size) 创建具有指定缓冲区大小的 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。
参数:
InputStream in:字节输入流
我们可以传递FileInputStream,缓冲流会给FileInputStream增加一个缓冲区,提高FileInputStream的读取效率
int size:指定缓冲流内部缓冲区的大小,不指定默认
使用步骤(重点):
1.创建FileInputStream对象,构造方法中绑定要读取的数据源
2.创建BufferedInputStream对象,构造方法中传递FileInputStream对象,提高FileInputStream对象的读取效率
3.使用BufferedInputStream对象中的方法read,读取文件
4.释放资源
//1.创建FileInputStream对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("src\\BufferedOutputStream\\demo.txt");
//2.创建BufferedInputStream对象,构造方法中传递FileInputStream对象,提高FileInputStream对象的读取效率
BufferedInputStream bis = new BufferedInputStream(fis);
//3.使用BufferedInputStream对象中的方法read,读取文件
int len = 0;
byte[] arr = new byte[1024];
while ((len = bis.read(arr)) != -1) {
System.out.println(new String(arr,0,len));
}
// 4.释放资源
bis.close();
字符缓冲流
java.io.BufferedWriter extends Writer
继承自父类的共性成员方法:
- void write(int c) 写入单个字符。
- void write(char[] cbuf)写入字符数组。
- abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
- void write(String str)写入字符串。
- void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
- void flush()刷新该流的缓冲。
- void close() 关闭此流,但要先刷新它。
构造方法:
BufferedWriter(Writer out) 创建一个使用默认大小输出缓冲区的缓冲字符输出流。
BufferedWriter(Writer out, int sz) 创建一个使用给定大小输出缓冲区的新缓冲字符输出流。
参数:
Writer out:字符输出流
我们可以传递FileWriter,缓冲流会给FileWriter增加一个缓冲区,提高FileWriter的写入效率
int sz:指定缓冲区的大小,不写默认大小
特有的成员方法:
void newLine() 写入一个行分隔符。会根据不同的操作系统,获取不同的行分隔符
换行:换行符号
windows:\r\n
linux:/n
mac:/r
使用步骤:
1.创建字符缓冲输出流对象,构造方法中传递字符输出流
2.调用字符缓冲输出流中的方法write,把数据写入到内存缓冲区中
3.调用字符缓冲输出流中的方法flush,把内存缓冲区中的数据,刷新到文件中
4.释放资源
// 1.创建字符缓冲输出流对象,构造方法中传递字符输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("src\\BufferedWriter\\demo.txt"));
// 2.调用字符缓冲输出流中的方法write,把数据写入到内存缓冲区中
bw.write("BufferedWriter是字节缓冲输出流");
// 使用对象自带的方法newLine()进行换行;
bw.newLine();
bw.write("第二行的内容");
// 3.调用字符缓冲输出流中的方法flush,把内存缓冲区中的数据,刷新到文件中
bw.flush();
// 4.释放资源
bw.close();
java.io.BufferedReader extends Reader
继承自父类的共性成员方法:
int read() 读取单个字符并返回。
int read(char[] cbuf)一次读取多个字符,将字符读入数组。
void close() 关闭该流并释放与之关联的所有资源。
构造方法:
BufferedReader(Reader in) 创建一个使用默认大小输入缓冲区的缓冲字符输入流。
BufferedReader(Reader in, int sz) 创建一个使用指定大小输入缓冲区的缓冲字符输入流。
参数:
Reader in:字符输入流
我们可以传递FileReader,缓冲流会给FileReader增加一个缓冲区,提高FileReader的读取效率
特有的成员方法:
String readLine() 读取一个文本行。读取一行数据
行的终止符号:通过下列字符之一即可认为某行已终止:换行 ('\n')、回车 ('\r') 或回车后直接跟着换行(\r\n)。
返回值:
包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回 null(并不是返回-1)
使用步骤:
1.创建字符缓冲输入流对象,构造方法中传递字符输入流
2.使用字符缓冲输入流对象中的方法read/readLine读取文本
3.释放资源
// 1.创建字符缓冲输入流对象,构造方法中传递字符输入流
BufferedReader br = new BufferedReader(new FileReader("src\\BufferedWriter\\demo.txt"));
// 2.使用字符缓冲输入流对象中的方法read/readLine读取文本
// String line = br.readLine();
// System.out.println(line);
// System.out.println(br.readLine());
// System.out.println(br.readLine());//第三行没有内容所以是null
/*
发现以上读取是一个重复的过程,所以可以使用循环优化
不知道文件中有多少行数据,所以使用while循环
while的结束条件,读取到null结束
*/
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
// 3.释放资源
br.close();
转换流
字符编码
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制 数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照 某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本f符 号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。
字符编码 Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。
字符集
字符集 Charset :也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符 号、数字等。 计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符 集有ASCII字符集、GBK字符集、Unicode字符集等。
ASCII字符集 :
ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁 字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显 示字符(英文大小写字符、阿拉伯数字和西文符号)。 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits) 表示一个字符,共256字符,方便支持欧洲常用字符。 ISO-8859-1字符集: 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。 ISO-5559-1使用单字节编码,兼容ASCII编码。
GBxxx字符集:
GB就是国标的意思,是为了显示中文而设计的一套字符集。
GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时, 就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文 的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这 就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了 21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。
GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节 组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。
Unicode字符集 :
Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国 码。 它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF32。最为常用的UTF-8编码。
UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用 中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以, 我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:
1. 128个US-ASCII字符,只需一个字节编码。
2. 拉丁文等字符,需要二个字节编码。
3. 大部分常用字(含中文),使用三个字节编码。
4. 其他极少使用的Unicode辅助字符,使用四字节编码。
使用FileReader或FileWrite和使用InputStreamReader或OutputStreamWriter的区别
OutputStreamWriter
java.io.OutputStreamWriter extends Writer
OutputStreamWriter: 是字符流通向字节流的桥梁:可使用指定的 charset 将要写入流中的字符编码成字节。(编码:把能看懂的变成看不懂)
继续自父类的共性成员方法:
- void write(int c) 写入单个字符。
- void write(char[] cbuf)写入字符数组。
- abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
- void write(String str)写入字符串。
- void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
- void flush()刷新该流的缓冲。
- void close() 关闭此流,但要先刷新它。
构造方法:
OutputStreamWriter(OutputStream out)创建使用默认字符编码的 OutputStreamWriter。
OutputStreamWriter(OutputStream out, String charsetName) 创建使用指定字符集的 OutputStreamWriter。
参数:
OutputStream out:字节输出流,可以用来写转换之后的字节到文件中
String charsetName:指定的编码表名称,不区分大小写,可以是utf-8/UTF-8,gbk/GBK,...不指定默认使用UTF-8
使用步骤:
1.创建OutputStreamWriter对象,构造方法中传递字节输出流和指定的编码表名称
2.使用OutputStreamWriter对象中的方法write,把字符转换为字节存储缓冲区中(编码)
3.使用OutputStreamWriter对象中的方法flush,把内存缓冲区中的字节刷新到文件中(使用字节流写字节的过程)
4.释放资源
// 1.创建OutputStreamWriter对象,构造方法中传递字节输出流和指定的编码表名称
// OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("src\\OutputStreamWriter \\demoUTf-8.txt"), "utf-8");
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("src\\OutputStreamWriter\\demoUTf-8.txt"));//默认也是utf-8
// 2.使用OutputStreamWriter对象中的方法write,把字符转换为字节存储缓冲区中(编码)
osw.write("你好");
// 3.使用OutputStreamWriter对象中的方法flush,把内存缓冲区中的字节刷新到文件中(使用字节流写字节的过程)
osw.flush();
// 4.释放资源
osw.close();
InputStreamReader
java.io.InputStreamReader extends Reader
InputStreamReader:是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。(解码:把看不懂的变成能看懂的)
继承自父类的共性成员方法:
int read() 读取单个字符并返回。
int read(char[] cbuf)一次读取多个字符,将字符读入数组。
void close() 关闭该流并释放与之关联的所有资源。
构造方法:
InputStreamReader(InputStream in) 创建一个使用默认字符集的 InputStreamReader。
InputStreamReader(InputStream in, String charsetName) 创建使用指定字符集的 InputStreamReader。
参数:
InputStream in:字节输入流,用来读取文件中保存的字节
String charsetName:指定的编码表名称,不区分大小写,可以是utf-8/UTF-8,gbk/GBK,...不指定默认使用UTF-8
使用步骤:
1.创建InputStreamReader对象,构造方法中传递字节输入流和指定的编码表名称
2.使用InputStreamReader对象中的方法read读取文件
3.释放资源
注意事项:
构造方法中指定的编码表名称要和文件的编码相同,否则会发生乱码
// 读取utf-8的文件
// 1.创建InputStreamReader对象,构造方法中传递字节输入流和指定的编码表名称
InputStreamReader isr = new InputStreamReader(new FileInputStream("src\\OutputStreamWriter\\demoUTf-8.txt"), "utf-8");//默认不写也是utf-8编码,要是想要按照gbk来解码则只需要将utf-8改成gbk
// 2.使用InputStreamReader对象中的方法read读取文件
int len = -1;
while ((len = isr.read()) != -1) {
System.out.println((char)len);
}
// 3.释放资源
isr.close();
序列化流
当要将一个对象从内存写入到硬盘中去的时候,我们就需要使用到序列化流,将对象以字节的形式写入到硬盘中去,而从硬盘中读取一个对象到内存则使用的是一个反序列化流
ObjectOutputStream:序列化
java.io.ObjectOutputStream extends OutputStream
ObjectOutputStream:对象的序列化流
作用:把对象以流的方式写入到文件中保存
构造方法:
ObjectOutputStream(OutputStream out) 创建写入指定 OutputStream 的 ObjectOutputStream。
参数:
OutputStream out:字节输出流
特有的成员方法:
void writeObject(Object obj) 将指定的对象写入 ObjectOutputStream。
注意:
在进行序列化之前,要被序列化的对象一定要继承Serializable接口
序列化和反序列化的时候,会抛出NotSerializableException没有序列化异常
类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。
Serializable接口也叫标记型接口
要进行序列化和反序列化的类必须实现Serializable接口,就会给类添加一个标记
当我们进行序列化和反序列化的时候,就会检测类上是否有这个标记
有:就可以序列化和反序列化
没有:就会抛出 NotSerializableException异常
去市场买肉-->肉上有一个蓝色章(检测合格)-->放心购买-->买回来怎么吃随意
使用步骤:
1.创建ObjectOutputStream对象,构造方法中传递字节输出流
2.使用ObjectOutputStream对象中的方法writeObject,把对象写入到文件中
3.释放资源
// 1、创建ObjectOutputStream对象,构造方法中传递字节输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src\\ObjectOutputStream\\demo01.txt"));
// 2.使用ObjectOutputStream对象中的方法writeObject,把对象写入到文件中
oos.writeObject(new Person("ljy",18));
// 3、释放资源
oos.close();
ObjectInputStream:反序列化
java.io.ObjectInputStream extends InputStream
ObjectInputStream:对象的反序列化流
作用:把文件中保存的对象,以流的方式读取出来使用
构造方法:
ObjectInputStream(InputStream in) 创建从指定 InputStream 读取的 ObjectInputStream。
参数:
InputStream in:字节输入流
特有的成员方法:
Object readObject() 从 ObjectInputStream 读取对象。
readObject方法声明抛出了ClassNotFoundException(class文件找不到异常)
当不存在对象的class文件时抛出此异常
反序列化的前提:
1.类必须实现Serializable
2.必须存在类对应的class文件
使用步骤:
1.创建ObjectInputStream对象,构造方法中传递字节输入流
2.使用ObjectInputStream对象中的方法readObject读取保存对象的文件
3.释放资源
4.使用读取出来的对象(打印)
//1.创建ObjectInputStream对象,构造方法中传递字节输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src\\ObjectOutputStream\\demo01.txt"));
//2.使用ObjectInputStream对象中的方法readObject读取保存对象的文件
Object o = ois.readObject();
//3.释放资源
ois.close();
//4.使用读取出来的对象(打印)
System.out.println(o);
Person p = (Person)o;
System.out.println(p.getName()+p.getAge());
每当我们修改了对象类的时候,jvm都会自动给这个类进行序列号修改,而当我们使用反序列化,jvm会自动的对比已经序列化了的对象文件的序列号,要是两个序列号不一样,则会抛出异常InvalidClassException,要是两个序列号一样,才会继续执行,而当我们在改变原来那个对象类的时候,又不想让对象类的序列号修改,这时候我们可以使用Serializable 接口给需要序列化的类,提供了一个序列版本号。 serialVersionUID 该版本号的目的在于验证序 列化的对象和对应类是否版本匹配。
transient关键字
要是不想让某个成员变量不想被序列化,则可以给这个成员变量添加transient关键字
static关键字:静态关键字
静态优先于非静态加载到内存中(静态优先于对象进入到内存中)
被static修饰的成员变量不能被序列化的,序列化的都是对象
private static int age;
oos.writeObject(new Person("小美女",18));
Object o = ois.readObject();
Person{name='小美女', age=0}
transient关键字:瞬态关键字
被transient修饰成员变量,不能被序列化
private transient int age;
oos.writeObject(new Person("小美女",18));
Object o = ois.readObject();
Person{name='小美女', age=0}
打印流:
java.io.PrintStream:打印流
PrintStream 为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。
PrintStream特点:
1.只负责数据的输出,不负责数据的读取
2.与其他输出流不同,PrintStream 永远不会抛出 IOException
3.有特有的方法,print,println
void print(任意类型的值)
void println(任意类型的值并换行)
构造方法:
PrintStream(File file):输出的目的地是一个文件
PrintStream(OutputStream out):输出的目的地是一个字节输出流
PrintStream(String fileName) :输出的目的地是一个文件路径
PrintStream extends OutputStream
继承自父类的成员方法:
- public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
- public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
- public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
- public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
- public abstract void write(int b) :将指定的字节输出流。
注意:
如果使用继承自父类的write方法写数据,那么查看数据的时候会查询编码表 97->a
如果使用自己特有的方法print/println方法写数据,写的数据原样输出 97->97
//创建打印流PrintStream对象,构造方法中绑定要输出的目的地
PrintStream ps = new PrintStream("src\\PrintStream\\print.txt");
//如果使用继承自父类的write方法写数据,那么查看数据的时候会查询编码表 97->a
ps.write(97);
//如果使用自己特有的方法print/println方法写数据,写的数据原样输出 97->97
ps.println(97);
ps.println(8.8);
ps.println('a');
ps.println("HelloWorld");
ps.println(true);
//释放资源
ps.close();
我们也可以使用System.SetOut(PrintStream ps)方法,将控制台输出转换成在打印流输出
可以改变输出语句的目的地(打印流的流向)
输出语句,默认在控制台输出
使用System.setOut方法改变输出语句的目的地改为参数中传递的打印流的目的地
static void setOut(PrintStream out)重新分配“标准”输出流。
System.out.println("我是在控制台输出");
PrintStream ps = new PrintStream("src\\PrintStream\\目的地是打印流.txt");
System.setOut(ps);//把输出语句的目的地改变为打印流的目的地
System.out.println("我在打印流的目的地中输出");
ps.close();
十四、网络编程
软件结构
C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。
B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。
两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机 的通信的程序。
网络通信协议
网络通信协议:通信协议是对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就 好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了 统一规定,通信双方必须同时遵守,最终完成数据交换。
TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是 Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它 的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的 协议来完成自己的需求。
协议分类
UDP:用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需 要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应 用中,例如视频会议、QQ聊天等。
TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前, 在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。每次传输数据之前都会进行三次握手。完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可 以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。
三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可 靠。 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。 第三次握手,客户端再次向服务器端发送确认信息,确认连接。
网络编程三要素
1、协议
以上全是协议
2、IP地址
指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设 备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。
IP地址分类:
IPv4:是一个32位的二进制数,通常被分为4个字节,表示成 a.b.c.d 的形式,例如 192.168.65.100 。其 中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。
IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。 有资料显示,全球IPv4地址在2011年2月分配完毕。 为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进 制数,表示成 ABCD:EF01:2345:6789:ABCD:EF01:2345:6789 ,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
关于IP地址的常用命令:
查看本机IP地址,在控制台输入:ipconfig
检查网络是否连通,在控制台输入:ping 空格 IP地址 例:ping 220.181.57.216
本机的IP地址:127.0.0.1 、 localhost 。两个都可以查到本机的IP地址
3、端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分 这些进程呢? 如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。
端口号:用两个字节表示的整数,它的取值范围是0~65535。其中,0~1023之间的端口号用于一些知名的网 络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会 导致当前程序启动失败。
利用 协议 + IP地址 + 端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。
TCP通信(C/S)
TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。
两端通信时步骤:
1. 服务端程序,需要事先启动,等待客户端的连接。
2. 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
在Java中,提供了两个类用于实现TCP通信程序:
1. 客户端: java.net.Socket 类表示。创建 Socket 对象,向服务端发出连接请求,服务端响应请求,两者建 立连接开始通信。
2. 服务端: java.net.ServerSocket 类表示。创建 ServerSocket 对象,相当于开启一个服务,并等待客户端 的连接。
Socket类
表示客户端的类:
java.net.Socket:此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
套接字:包含了IP地址和端口号的网络单位
构造方法:
Socket(String host, int port) 创建一个流套接字并将其连接到指定主机上的指定端口号。
参数:
String host:服务器主机的名称/服务器的IP地址
int port:服务器的端口号
成员方法:
OutputStream getOutputStream() 返回此套接字的输出流。
InputStream getInputStream() 返回此套接字的输入流。
void close() 关闭此套接字。
实现步骤:
1.创建一个客户端对象Socket,构造方法绑定服务器的IP地址和端口号
2.使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
3.使用网络字节输出流OutputStream对象中的方法write,给服务器发送数据
4.使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
5.使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
6.释放资源(Socket)
注意:
1.客户端和服务器端进行交互,必须使用Socket中提供的网络流,不能使用自己创建的流对象
2.当我们创建客户端对象Socket的时候,就会去请求服务器和服务器经过3次握手建立连接通路
这时如果服务器没有启动,那么就会抛出异常ConnectException: Connection refused: connect
如果服务器已经启动,那么就可以进行交互了
// 1.创建一个客户端对象Socket,构造方法绑定服务器的IP地址和端口号
Socket soc = new Socket("127.0.0.1", 8888);//连接本地的计算机,随便给一个没有被占用的端口号
// 2.使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
OutputStream ops = soc.getOutputStream();
// 3.使用网络字节输出流OutputStream对象中的方法write,给服务器发送数据
ops.write("你好,服务器".getBytes());
// 4.使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
InputStream is = soc.getInputStream();
// 5.使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
byte[] arr = new byte[1024];
int len = is.read(arr);
System.out.println(new String(arr,0,len));
// 6.释放资源(Socket)
soc.close();
ServerSocket类
TCP通信的服务器端:接收客户端的请求,读取客户端发送的数据,给客户端回写数据
表示服务器的类:
java.net.ServerSocket:此类实现服务器套接字。
构造方法:
ServerSocket(int port) 创建绑定到特定端口的服务器套接字。
服务器端必须明确一件事情,必须得知道是哪个客户端请求的服务器
所以可以使用accept方法获取到请求的客户端对象Socket
成员方法:
Socket accept() 侦听并接受到此套接字的连接。
服务器的实现步骤:
1.创建服务器ServerSocket对象和系统要指定的端口号
2.使用ServerSocket对象中的方法accept,获取到请求的客户端对象Socket
3.使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
4.使用网络字节输入流InputStream对象中的方法read,读取客户端发送的数据
5.使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
6.使用网络字节输出流OutputStream对象中的方法write,给客户端回写数据
7.释放资源(Socket,ServerSocket)
// 1.创建服务器ServerSocket对象和系统要指定的端口号
ServerSocket server = new ServerSocket(8888);
// 2.使用ServerSocket对象中的方法accept,获取到请求的客户端对象Socket
Socket socker = server.accept();
// 3.使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
InputStream is = socker.getInputStream();
// 4.使用网络字节输入流InputStream对象中的方法read,读取客户端发送的数据
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes, 0, len));
// 5.使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
OutputStream os = socker.getOutputStream();
// 6.使用网络字节输出流OutputStream对象中的方法write,给客户端回写数据
os.write("收到".getBytes());
// 7.释放资源(Socket,ServerSocket)
socker.close();
server.close();
B/S(了解即可)
十五、函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口。(当然接口中可以包含其他的方法:默认,静态,私有)。
格式:
修饰符 interface 接口名称{
public abstract 返回值类型 方法名称(可选参数信息);
//由于接口中public abstract可以省略,所以可以写成
void method()
//其他非抽象方法内容
}
与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上:
要是是一个函数式接口,则编译通过,要是不是函数式接口,则在编程过程中编译报错
//使用FunctionalInterface注解来标记这个函数接口
@FunctionalInterface
public interface demo01 {
void method();
}
函数式编程
Lambda的延迟执行
有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以 作为解决方案,提升性能。
/**
日志案例
发现以下代码存在的一些性能浪费的问题
调用showLog方法,传递的第二个参数是一个拼接后的字符串
先把字符串拼接好,然后在调用showLog方法
showLog方法中如果传递的日志等级不是1级
那么就不会是如此拼接后的字符串
所以感觉字符串就白拼接了,存在了浪费
*/
public static void riZhi(int day,String mess) {
if (day == 1) {
System.out.println(mess);
}
}
public static void main(String[] args) {
//定义三个日志信息
String msg1 = "Hello";
String msg2 = "World";
String msg3 = "Java";
//调用showLog方法,传递日志级别和日志信息
riZhi(2,msg1+msg2+msg3);
}
这段代码存在问题:无论级别是否满足要求,作为riZhi方法的第二个参数,三个字符串一定会首先被拼接并传入方法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。
通过Lambda表达式改善的代码:
Lambda表达式里面的代码只有接口的的方法被调用了才会执行
public static void riZhi(int day,riZhiInterface inter) {
if (day ==1) {
System.out.println(inter.riZhiNeiRong());
}
}
public static void main(String[] args) {
//定义三个日志信息
String msg1 = "Hello";
String msg2 = "World";
String msg3 = "Java";
// 调用日志
riZhi(1, () -> {
return msg1 + msg2 + msg3;
});
// Lambda表达式只有在接口的方法被调用的时候才会执行里面的内容。要是接口的方法没有被调用,则Lambda表达式里面的代码不会执行
}
使用Lambda作为参数
例如java.lang.Runnable接口就是一个函数式接口,
假设有一个startThread方法使用该接口作为参数,那么就可以使用Lambda进行传参。
这种情况其实和Thread类的构造方法参数为Runnable没有本质区别。
//定义一个方法startThread,方法的参数使用函数式接口Runnable
public static void startThread(Runnable run){
//开启多线程
new Thread(run).start();
}
public static void main(String[] args) {
// 使用匿名内部类
startThread(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程启动");
}
});
// 使用Lambda表达式
startThread(() -> {
System.out.println(Thread.currentThread().getName() + "线程启动了");
});
// 优化Lambda表达式
startThread(() -> System.out.println(Thread.currentThread().getName() + "线程启动了了"));
}
使用Lambda作为返回值
如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。
当需要通过一个方法来获取一个java.util.Comparator接口类型的对象作为排序器时,就可以调该方法获取
public static Comparator<String> getComparator(){
//方法的返回值类型是一个接口,那么我们可以返回这个接口的匿名内部类
/*return new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
//按照字符串的降序排序
return o2.length()-o1.length();
}
};*/
//方法的返回值类型是一个函数式接口,所有我们可以返回一个Lambda表达式
/*return (String o1, String o2)->{
//按照字符串的降序排序
return o2.length()-o1.length();
};*/
//继续优化Lambda表达式
return (o1, o2)->o2.length()-o1.length();
}
public static void main(String[] args) {
//创建一个字符串数组
String[] arr = {"aaa","b","cccccc","dddddddddddd"};
//输出排序前的数组
System.out.println(Arrays.toString(arr));//[aaa, b, cccccc, dddddddddddd]
//调用Arrays中的sort方法,对字符串数组进行排序
Arrays.sort(arr,getComparator());
//输出排序后的数组
System.out.println(Arrays.toString(arr));//[dddddddddddd, cccccc, aaa, b]
}
常用函数式接口
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function 包中被提供。 下面是最简单的几个接口及使用示例。
Supplier生产(创造)型接口
java.util.function.Supplier<T>接口仅包含一个无参的方法:T get()。用来获取一个泛型参数指定类型的对象数据。
Supplier<T>接口被称之为生产型接口,指定接口的泛型是什么类型,那么接口中的get方法就会生产什么类型的数据
//定义一个方法,方法的参数传递Supplier<T>接口,泛型执行String,get方法就会返回一个String
public static String getString(Supplier<String> sup) {
return sup.get();
}
public static void main(String[] args) {
// 使用getString方法来生产一个字符串,方法的参数式函数式接口,所以可以使用Lambda表达式
String a = getString(() -> {
return "ljy";
});
System.out.println(a);
// 使用Lambda简化
String b = getString(() -> "zjl");
System.out.println(b);
}
Consumer消费(使用)型接口
java.util.function.Consumer<T>接口则正好与Supplier接口相反,
它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。
Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。
Consumer接口是一个消费型接口,泛型执行什么类型,就可以使用accept方法消费什么类型的数据
至于具体怎么消费(使用),需要自定义(输出,计算....)
抽象方法accept
public static void method(String name , Consumer<String> con) {
con.accept(name);
}
public static void main(String[] args) {
// 第一个参数是ljy,Lambda表达式里面的name指的就是ljy
// 其实在Lambda表达式括号里面传参的名称随便是什么都可以,只是调用的时候方便就可以了
method("ljy", (name) -> {
// 第一种消费方式:打印
System.out.println(name);
// 第二种消费方式:把字符串进行反转输出
String reName = new StringBuffer(name).reverse().toString();
System.out.println(reName);
});
}
默认方法:andThen
Consumer接口的默认方法andThen
作用:需要两个Consumer接口,可以把两个Consumer接口组合到一起,在对数据进行消费
例如:
Consumer<String> con1
Consumer<String> con2
String s = "hello";
con1.accept(s);
con2.accept(s);
连接两个Consumer接口 再进行消费
con1.andThen(con2).accept(s); 谁写前边谁先消费
两个红色的代码的效果是一样的
//定义一个方法,方法的参数传递一个字符串和两个Consumer接口,Consumer接口的泛型使用字符串
public static void method(String s, Consumer<String> con1 ,Consumer<String> con2){
//con1.accept(s);
//con2.accept(s);
//使用andThen方法,把两个Consumer接口连接到一起,在消费数据
con1.andThen(con2).accept(s);//con1连接con2,先执行con1消费数据,在执行con2消费数据
}
public static void main(String[] args) {
//调用method方法,传递一个字符串,两个Lambda表达式
method("Hello",
(t)->{
//消费方式:把字符串转换为大写输出
System.out.println(t.toUpperCase());
},
(t)->{
//消费方式:把字符串转换为小写输出
System.out.println(t.toLowerCase());
});
}
Predicate判断型接口
java.util.function.Predicate<T>接口
作用:对某种数据类型的数据进行判断,结果返回一个boolean值
Predicate接口中包含一个抽象方法:
boolean test(T t):用来对指定数据类型数据进行判断的方法
结果:
符合条件,返回true
不符合条件,返回false
抽象方法test:
public static boolean checkString(String s, Predicate<String> pre){
return pre.test(s);
}
public static void main(String[] args) {
boolean b = checkString("ljy", (s) -> {
return s.length() > 5;
});
System.out.println(b);
}
// 简化之后的Lambda表达式
boolean b = checkString("ljy", s -> s.length() > 5 );
逻辑表达式:可以连接多个判断的条件
&&:与运算符,有false则false
||:或运算符,有true则true
!:非(取反)运算符,非真则假,非假则真
默认方法and:
/*
定义一个方法,方法的参数,传递一个字符串
传递两个Predicate接口
一个用于判断字符串的长度是否大于5
一个用于判断字符串中是否包含a
两个条件必须同时满足
*/
public static boolean method(String s,Predicate<String> pre1,Predicate<String> pre2) {
// return pre1.test(s) && pre2.test(s);
return pre1.and(pre2).test(s);//等价于return pre1.test(s) && pre2.test(s);
}
public static void main(String[] args) {
String a = "ljyl";
boolean b = method(a, (String str) -> {
// 字符串长度是否大于5
return str.length() > 5;
}, (String str) -> {
// 字符串是否包含l
return str.contains("l");
});
System.out.println(b);
}
默认方法or:
/*
定义一个方法,方法的参数,传递一个字符串
传递两个Predicate接口
一个用于判断字符串的长度是否大于5
一个用于判断字符串中是否包含a
满足一个条件即可
*/
public static boolean checkString(String s, Predicate<String> pre1, Predicate<String> pre2){
//return pre1.test(s) || pre2.test(s);
return pre1.or(pre2).test(s);//等价于return pre1.test(s) || pre2.test(s);
}
public static void main(String[] args) {
//定义一个字符串
String s = "bc";
//调用checkString方法,参数传递字符串和两个Lambda表达式
boolean b = checkString(s,(String str)->{
//判断字符串的长度是否大于5
return str.length()>5;
},(String str)->{
//判断字符串中是否包含a
return str.contains("a");
});
System.out.println(b);
}
默认方法negate
/*
定义一个方法,方法的参数,传递一个字符串
使用Predicate接口判断字符串的长度是否大于5
*/
public static boolean checkString(String s, Predicate<String> pre){
//return !pre.test(s);
return pre.negate().test(s);//等效于return !pre.test(s);
}
public static void main(String[] args) {
//定义一个字符串
String s = "abc";
//调用checkString方法,参数传递字符串和Lambda表达式
boolean b = checkString(s,(String str)->{
//判断字符串的长度是否大于5,并返回结果
return str.length()>5;
});
System.out.println(b);
}
Function 转换型接口
java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,
前者称为前置条件,后者称为后置条件。
Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。
使用的场景例如:将String类型转换为Integer类型。
抽象方法:apply
/*
定义一个方法
方法的参数传递一个字符串类型的整数
方法的参数传递一个Function接口,泛型使用<String,Integer>
使用Function接口中的方法apply,把字符串类型的整数,转换为Integer类型的整数
*/
public static void change(String s, Function<String,Integer> fun){
//Integer in = fun.apply(s);
int in = fun.apply(s);//自动拆箱 Integer->int
System.out.println(in);
}
public static void main(String[] args) {
//定义一个字符串类型的整数
String s = "1234";
//调用change方法,传递字符串类型的整数,和Lambda表达式
change(s,(String str)->{
//把字符串类型的整数,转换为Integer类型的整数返回
return Integer.parseInt(str);
});
//优化Lambda
change(s,str->Integer.parseInt(str));
}
默认方法:andThen
Function接口中的默认方法andThen:用来进行组合操作
需求:
把String类型的"123",转换为Inteter类型,把转换后的结果加10
把增加之后的Integer类型的数据,转换为String类型
分析:
转换了两次
第一次是把String类型转换为了Integer类型
所以我们可以使用Function<String,Integer> fun1
Integer i = fun1.apply("123")+10;
第二次是把Integer类型转换为String类型
所以我们可以使用Function<Integer,String> fun2
String s = fun2.apply(i);
我们可以使用andThen方法,把两次转换组合在一起使用
String s = fun1.andThen(fun2).apply("123");
fun1先调用apply方法,把字符串转换为Integer
fun2再调用apply方法,把Integer转换为字符串
/*
定义一个方法
参数串一个字符串类型的整数
参数再传递两个Function接口
一个泛型使用Function<String,Integer>
一个泛型使用Function<Integer,String>
*/
public static void change(String s, Function<String,Integer> fun1,Function<Integer,String> fun2){
String ss = fun1.andThen(fun2).apply(s);
System.out.println(ss);
}
public static void main(String[] args) {
//定义一个字符串类型的整数
String s = "123";
//调用change方法,传递字符串和两个Lambda表达式
change(s,(String str)->{
//把字符串转换为整数+10
return Integer.parseInt(str)+10;
},(Integer i)->{
//把整数转换为字符串
return i+"";
});
//优化Lambda表达式
change(s,str->Integer.parseInt(str)+10,i->i+"");
}
十六、Stream流
说到Stream便容易想到I/O Stream,而实际上,谁规定“流”就一定是“IO流”呢?在Java 8中,得益于Lambda所带 来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。
使用Stream流会使代码变得更简洁易懂,关注的是做什么,而不是怎么做
流式思想概述
整体来看,流式思想类似于工厂车间的“生产流水线”。
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤 方案,然后再按照方案去执行它。
先将这个模型建立起来,只有当执行到count的时候,才会将之前的代码执行,这得益于Lambda表达式的延迟执行的特征
和以前的Collection操作不同, Stream操作还有两个基础的特征:
Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
内部迭代: 以前对集合遍历都是通过Iterator或者增强for的方式, 显式的在集合外部进行迭代, 这叫做外部迭 代。 Stream提供了内部迭代的方式,流可以直接调用遍历方法。
当使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source)→ 数据转换→执行操作获取想要的结果
每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换并且原来的那个流不变),这就允许对其操作可以像链条一样排列,变成一个管道。
获取流
java.util.stream.Stream<T>是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)
获取一个流非常简单,有以下几种常用的方式:
- 所有的Collection集合都可以通过stream默认方法获取流;
default Stream<E> stream()
//把集合转换为Stream流
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();
Map<String,String> map = new HashMap<>();
//获取键,存储到一个Set集合中
Set<String> keySet = map.keySet();
Stream<String> stream3 = keySet.stream();
//获取值,存储到一个Collection集合中
Collection<String> values = map.values();
Stream<String> stream4 = values.stream();
//获取键值对(键与值的映射关系 entrySet)
Set<Map.Entry<String, String>> entries = map.entrySet();
Stream<Map.Entry<String, String>> stream5 = entries.stream();
- Stream接口的静态方法of可以获取数组对应的流。
static <T> Stream<T> of(T... values)
//把数组转换为Stream流
Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5);
//可变参数可以传递数组
Integer[] arr = {1,2,3,4,5};
Stream<Integer> stream7 = Stream.of(arr);
String[] arr2 = {"a","bb","ccc"};
Stream<String> stream8 = Stream.of(arr2);
常用方法:
延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)
终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。本小节中,终结方法包括 count 和 forEach 方法。
特点:
Stream流属于管道流,只能被消费(使用)一次
第一个Stream流调用完毕方法,数据就会流转到下一个Stream上
而这时第一个Stream流已经使用完毕,就会关闭了
所以第一个Stream流就不能再调用方法了
IllegalStateException: stream has already been operated upon or closed
逐一处理forEach
作用:用来遍历处理流中的信息
void forEach(Consumer<? super T> action);
该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理。
Consumer接口是一个消费型的函数式接口,可以传递Lambda表达式,消费数据
简单记:
forEach方法,用来遍历流中的数据
是一个终结方法,遍历之后就不能继续调用Stream流中的其他方法
Stream<String> str1 = Stream.of("ljy","zql","sh","cp");
str1.forEach((String name) -> {
System.out.println(name);
});
过滤filter
作用:用于对Stream流中的数据进行过滤
Stream<T> filter(Predicate<? super T> predicate);
filter方法的参数Predicate是一个函数式接口,所以可以传递Lambda表达式,对数据进行过滤
Predicate中的抽象方法:
boolean test(T t);
//创建一个Stream流
Stream<String> stream = Stream.of("张三丰", "张翠山", "赵敏", "周芷若", "张无忌");
//对Stream流中的元素进行过滤,只要姓张的人
Stream<String> stream2 = stream.filter((String name) -> {
return name.startsWith("张");
});
//遍历stream2流
stream2.forEach(name -> System.out.println(name));
映射map
作用:用于类型转换
如果需要将流中的元素映射到另一个流中,可以使用map方法.
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
Function中的抽象方法:
R apply(T t);
Stream<String> str1 = Stream.of("1","2","3","5");
//使用map方法,把字符串类型的整数,转换(映射)为Integer类型的整数
Stream<Integer> str2 = str1.map((String s) -> {
return Integer.parseInt(s);
});
// 遍历输出
str2.forEach(num -> System.out.println(num));
统计个数count:
作用:用于统计Stream流中元素的个数
long count();
count方法是一个终结方法,返回值是一个long类型的整数
所以不能再继续调用Stream流中的其他方法了
Stream<String> str1 = Stream.of("1","2","3","4");
// 使用count方法来获取到Stream的个数
long count = str1.count();
System.out.println(count);
只取前几个:limit
作用:用于截取流中的元素
limit方法可以对流进行截取,只取用前n个。方法签名:
Stream<T> limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作
limit方法是一个延迟方法,只是对流中的元素进行截取,返回的是一个新的流,所以可以继续调用Stream流中的其他方法
//获取一个Stream流
String[] arr = {"美羊羊","喜洋洋","懒洋洋","灰太狼","红太狼"};
Stream<String> stream = Stream.of(arr);
//使用limit对Stream流中的元素进行截取,只要前3个元素
Stream<String> stream2 = stream.limit(3);
//遍历stream2流
stream2.forEach(name-> System.out.println(name));
跳过前几个:skip
作用:用于跳过元素
如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流:
Stream<T> skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。
//获取一个Stream流
String[] arr = {"美羊羊","喜洋洋","懒洋洋","灰太狼","红太狼"};
Stream<String> stream = Stream.of(arr);
//使用skip方法跳过前3个元素
Stream<String> stream2 = stream.skip(3);
//遍历stream2流
stream2.forEach(name-> System.out.println(name));//灰太狼红太狼
组合:concat
Stream流中的常用方法_concat:用于把流组合到一起
如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
public static void main(String[] args) {
// 获取第一个流
Stream<Integer> str1 = Stream.of(1 , 2 , 3 , 4 , 5);
// 获取第二个流
Stream<Integer> str2 = Stream.of(6 , 7);
// 获取组合流
Stream<Integer> concat = Stream.concat(str1, str2);
concat.forEach(num -> System.out.println(num));
}
十七、方法引用
方法引用是对Lambda的一种优化写法
/*
分析:
Lambda表达式的目的,打印参数传递的字符串
把参数s,传递给了System.out对象,调用out对象中的方法println对字符串进行了输出
注意:
1.System.out对象是已经存在的
2.println方法也是已经存在的
所以我们可以使用方法引用来优化Lambda表达式
可以使用System.out方法直接引用(调用)println方法
*/
printString(System.out::println);
Lambda表达式写法: s -> System.out.println(s);
方法引用写法: System.out::println
第一种语义是指:拿到参数之后经Lambda之手,继而传递给 System.out.println 方法去处理。
第二种等效写法的语义是指:直接让 System.out 中的 println 方法来取代Lambda。两种写法的执行效果完全一样,而第二种方法引用的写法复用了已有方案,更加简洁。
注:Lambda 中 传递的参数 一定是方法引用中 的那个方法可以接收的类型,否则会抛出异常
通过对象名来引用成员方法
通过对象名引用成员方法
使用前提是对象名是已经存在的,成员方法也是已经存在
就可以使用对象名来引用成员方法
首先定义一个类(MethodRerObject),里面有一个成员方法printUpperCaseString(把字符串按照大写输出)
public class MethodRerObject {
//定义一个成员方法,传递字符串,把字符串按照大写输出
public void printUpperCaseString(String str){
System.out.println(str.toUpperCase());
}
}
在定义一个函数型接口
@FunctionalInterface
public interface Printable {
//定义字符串的抽象方法
void print(String s);
}
在测试类里面创建一个方法(printString),里面传递printable接口,调用接口中的print方法
public static void printString(Printable p){
p.print("Hello");
}
测试类的printString方法里面要是想要调用MethodRerObject对象里面的成员方法则要写成这样
//调用printString方法,方法的参数Printable是一个函数式接口,所以可以传递Lambda表达式
printString((s)->{
//创建MethodRerObject对象
MethodRerObject obj = new MethodRerObject();
//调用MethodRerObject对象中的成员方法printUpperCaseString,把字符串按照大写输出
obj.printUpperCaseString(s);
});
要是使用方法引用,则写成这样
/*
使用方法引用优化Lambda
对象是已经存在的MethodRerObject
成员方法也是已经存在的printUpperCaseString
所以我们可以使用对象名引用成员方法
*/
//创建MethodRerObject对象(在使用通过对象名来引用成员方法的时候,前提是这个对象必须已经得到创建)
MethodRerObject obj = new MethodRerObject();
printString(obj::printUpperCaseString);
通过类名称引用静态方法
类已经存在,静态成员方法也已经存在
就可以通过类名直接引用静态成员方法
先创建一个函数型接口,定义一个方法calsAbs
@FunctionalInterface
public interface Calcable {
//定义一个抽象方法,传递一个整数,对整数进行绝对值计算并返回
int calsAbs(int number);
}
在测试类中创建一个方法
//定义一个方法,方法的参数传递要计算绝对值的整数,和函数式接口Calcable
public static int method(int number,Calcable c){
return c.calsAbs(number);
}
在测试类中不使用方法引用来写Lambda表达式
//调用method方法,传递计算绝对值得整数,和Lambda表达式
int number = method(-10,(n)->{
//对参数进行绝对值得计算并返回结果
return Math.abs(n);
});
System.out.println(number);
使用方法引用
/*
使用方法引用优化Lambda表达式
Math类是存在的
abs计算绝对值的静态方法也是已经存在的
所以我们可以直接通过类名引用静态方法
*/
int number2 = method(-10,Math::abs);
System.out.println(number2);
通过super引用成员方法
如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代
先创建一个函数型接口
@FunctionalInterface
public interface Greetable {
//定义一个见面的方法
void greet();
}
在创建一个父类,里面定义一个方法
public class Human {
//定义一个sayHello的方法
public void sayHello(){
System.out.println("Hello 我是Human!");
}
}
在创建一个子类,继承父类,并且重写父类的sayHello方法
//子类重写父类sayHello的方法
@Override
public void sayHello() {
System.out.println("Hello 我是Man!");
}
在子类中定义一个方法,传递的是一个接口
//定义一个方法参数传递Greetable接口
public void method(Greetable g){
g.greet();
}
在子类中创建show方法,是调用method方法
public void show(){
//调用method方法,方法的参数Greetable是一个函数式接口,所以可以传递Lambda
method(()->{
//创建父类Human对象
Human h = new Human();
//调用父类的sayHello方法
h.sayHello();
});
//因为有子父类关系,所以存在的一个关键字super,代表父类,所以我们可以直接使用super调用父类的成员方法
method(()->{
super.sayHello();
});
}
因为这些方法都是在子类中创建,所以存在super关键字表示父类,但是我们可以更加的简化Lambda表达式,直接使用方法引用,也可以得到以上两种方法的结果
/*
使用super引用类的成员方法
super是已经存在的
父类的成员方法sayHello也是已经存在的
所以我们可以直接使用super引用父类的成员方法
*/
method(super::sayHello);
通过this引用成员方法
this代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用“this::成员方法”的格式来使用方法引用。
首先创建一个接口
@FunctionalInterface
public interface Richable {
//定义一个想买什么就买什么的方法
void buy();
}
在测试类中定义一个方法,方法是买房子
//定义一个买房子的方法
public void buyHouse(){
System.out.println("北京二环内买一套四合院!");
}
定义一个方法,参数是将接口传递进去
//定义一个结婚的方法,参数传递Richable接口
public void marry(Richable r){
r.buy();
}
定义一个方法,调用传递了接口的方法
//定义一个非常高兴的方法
public void soHappy(){
//调用结婚的方法,方法的参数Richable是一个函数式接口,传递Lambda表达式
marry(()->{
//使用this.成员方法,调用本类买房子的方法
this.buyHouse();
});
}
可以通过方法引用,改善Lambda表达式
//定义一个非常高兴的方法
public void soHappy(){
//调用结婚的方法,方法的参数Richable是一个函数式接口,传递Lambda表达式
// marry(()->{
// //使用this.成员方法,调用本类买房子的方法
// this.buyHouse();
// });
/*
使用方法引用优化Lambda表达式
this是已经存在的
本类的成员方法buyHouse也是已经存在的
所以我们可以直接使用this引用本类的成员方法buyHouse
*/
marry(this::buyHouse);
}
最后在测试类里面创建main方法,调用soHappy方法
public static void main(String[] args) {
// 创建一个对象,调用方法
new Husband().soHappy();}
类的构造器引用
由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示。
函数型接口:
@FunctionalInterface
public interface PersonBuilder {
//定义一个方法,根据传递的姓名,创建Person对象返回
Person builderPerson(String name);
}
创建一个Person类
public class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试类的方法:
//定义一个方法,参数传递姓名和PersonBuilder接口,方法中通过姓名创建Person对象
public static void printName(String name,PersonBuilder pb){
Person person = pb.builderPerson(name);
System.out.println(person.getName());
}
public static void main(String[] args) {
//调用printName方法,方法的参数PersonBuilder接口是一个函数式接口,可以传递Lambda
printName("迪丽热巴",(String name)->{
return new Person(name);
});
/*
使用方法引用优化Lambda表达式
构造方法new Person(String name) 已知
创建对象已知 new
就可以使用Person引用new创建对象
*/
printName("古力娜扎",Person::new);//使用Person类的带参构造方法,通过传递的姓名创建对象
}
这里的使用情况就是类只能有一个参数,不能传递多个
数组的构造器引用
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。如
定义一个函数型接口
@FunctionalInterface
public interface ArrayBuilder {
//定义一个创建int类型数组的方法,参数传递数组的长度,返回创建好的int类型数组
int[] builderArray(int length);
}
测试类:
/*
定义一个方法
方法的参数传递创建数组的长度和ArrayBuilder接口
方法内部根据传递的长度使用ArrayBuilder中的方法创建数组并返回
*/
public static int[] createArray(int length, ArrayBuilder ab){
return ab.builderArray(length);
}
public static void main(String[] args) {
//调用createArray方法,传递数组的长度和Lambda表达式
int[] arr1 = createArray(10,(len)->{
//根据数组的长度,创建数组并返回
return new int[len];
});
System.out.println(arr1.length);//10
/*
使用方法引用优化Lambda表达式
已知创建的就是int[]数组
数组的长度也是已知的
就可以使用方法引用
int[]引用new,根据参数传递的长度来创建数组
*/
int[] arr2 =createArray(10,int[]::new);
System.out.println(Arrays.toString(arr2));
System.out.println(arr2.length);//10
}
数据机构
栈
先进后出,像弹夹一样,最先进去的子弹最后射出
队列
先进先出
数组
查询快:数组的地址是连续的,我们通过数组的首地址可以找到数组,通过数组的索引值可以快速查找某一元素。
增删慢:数组的长度是固定的,我们想要增加/删除一个元素,必须创建一个新数组,把原数组的数据复制过来
链表
查询慢:链表中地址不是连续的,每次查询元素,都必须从头开始查询
增删快:链表结构,增加/删除一个元素,对链表的整体结构没有影响,所以增删快
链表中的每一个元素也称之为一个节点,一个节点包含了一个数据源(存储数组),两个指针域(存储地址)
单向链表:链表中只有一条链子,不能保证元素的顺序(存储元素和取出元素的顺序可能不一致)。
双向链表:链表中有两条链子,有一条链子是专门记录元素的顺序,是一个有序的集合
红黑树
内存划分
方法区里存放着都是方法还有成员变量、方法,而堆里只有成员变量没有方法,方法调用的都是地址,地址指向方法区里的方法