基本语法
标识符与关键字
本质就是符号,用来标记某个实体(内存空间),在不同的应用环境下有不同的含义,如做变量名,方法名
① 标识符只能由数字、字母(包括汉字)、下划线“_”、美元符号“$”组成,不能含有其它符号。
② 标识符不能以数字开始。
③ java 关键字和保留字不能作为标识符。
④ 标识符严格区分大小写。
⑤ 标识符理论上没有长度限制。
命名规范:
① 见名知意:看到这个单词就知道它表示什么,增强程序的可读性,例如:Student 则表示学生类型,User 则表示用户类型;
② 遵循驼峰命名方式:可以很好的分隔单词,每个单词之间会划清界限,同样也是增强程序的可读性,例如:getName 则表示获取名字,UserService 则表示用户业务类;
③ 类名、接口名每个单词首字母大写,这是遵守驼峰命名方式的;
④ 变量名、方法名首字母小写,后面每个单词首字母大写,这也是遵守驼峰命名方式的;
⑤ 常量名全部大写,单词和单词之间使用“_”衔接,为了表示清楚含义,不要怕单词
长,例如:INT_MAX_VALUE 则表示 int 类型最大值。
变量
字面量: 就是数据/数值,例如:1234,true,”abc”,‟中‟,3.14。
变量:内存当中存储数据最基本的单元,抽象理解:将数据放到内存当中,给这块内存空间起一个名字(用标识符),这就是变量。所以变量就是一块内存空间,这块空间有名字、有类型、有值,这也是变量必须具备的三要素。
java 语言中的变量要求必须先声明,再赋值才能访问,没有赋值则内存空间不开辟,无法访问
声明 数据类型 变量名;
赋值 变量名 = 值 ;
注意:成员变量有缺省默认值,局部变量没有,必须手动赋值
局部变量: 在方法体当中声明的变量以及方法的每一个参数
成员变量: 在方法体外,类体内声明的变量,成员变量声明时如果使用 static 关键字修饰的为静态成员变量(简称静态变量),如果没有 static 关键字修饰则称为实例成员变量(简称实例变量)
局部变量只在方法体当中有效,方法开始执行的时候局部变量的内存才会被分配,当方法执行结束之后,局部变量的内存就释放了。
Java遵循就近原则,对于不同作用域的同名变量,调用时会调用最近作用域的那个,如构造方法的形参与成员变量同名时,无法操作成员变量,方法体内都是对形参这个局部变量的操作
细节待补充....
数据类型
重点:
-
各种数据类型占用字节,默认缺省值
-
特定字面量有默认数据类型
-
数据类型转换
-
bsc特例
数据类型作用就是决定程序运行阶段给该变量分配多大的内存空间。
两大类:基本数据类型和引用数据类型
基本数据类型:
-
整数型(不带小数的数字):byte,short,int,long
-
浮点型(带小数的数字):float,double
-
字符型(文字,单个字符):char
-
布尔型(真和假):boolean
正在上传…重新上传取消正在上传…重新上传取消
(计算机中的数据都是用补码存储的,显示的时候再转换成原码,正数原码反码补码相同,负数不同)
一个字节表示范围为127~ -128,是因为补码中用(-128)代替了(-0),[1000 0000]补 就是-128
字符型
占用 2 个字节,char 类型的字面量必须使用半角的单引号括起来,只能包含一个字符,取值范围为[0-65535],char 和 short 都占用 2 个字节,但是 char 可以取到更大的正整数,因为 char 类型没有负数。
转义字符:\t、\n、\u、\、\',\",均只表示一个字符,\起转义的作用,其中\t 代表制表符,\n 是换行符,\\表示一个普通的\字符。单双引号在Java中有特殊的用法,会自动匹配第二个,所以用\'表示一个普通的',\"表示一个普通的"。如:’’‘会报错。
整型
整数型字面量默认当作int类型处理,也就是说在程序中只要遇到整数型的数字,该数字会默认被当做 int 类型来处理,默认为十进制,以 0 开始表示八进制,以 0x 开始表示十六进制,以 0b 开始表示二进制。如果想表示 long 类型则需要在字面量后面手动添加L/l,建议大写 L,因为小写 l 和 1 不好区分
注意:java程序在遇到特定的字面量时会先用默认的数据类型存储。
long a= 2147483648;报错,这是因为Java程序将这个整数型字面量按默认先存储为int型再去转,但是int已经存不下了,所以要加L,让Java直接存储long型。
当一个整数型的字面量没有超出 byte,short,char 的取值范围,可以将该字面量直接赋值给byte,short,char 类型的变量,如果超出范围则需要添加强制类型转换符
布尔型
在 Java 中布尔类型的变量值不能使用 1 和 0,只能使用 true和 false
可以直接接收一个关系运算表达式的结果
浮点型
float 单精度占 4 个字节,double 双精度占 8 个字节,默认double型存储,相对来说 double 精度要高一些。浮点型数据存储的是近似值。只要是浮点型的字面量,例如 1.0、3.14 等默认会被当做 double 类型来处理,如果想让程序将其当做 float 类型来处理,需要在字面量后面添加 f/F。
基本数据类型转换
八种基本数据类型中,除 boolean 类型不能转换,剩下七种类型之间都可以进行转换;
-
如果整数型字面量没有超出 byte,short,char 的取值范围,可以直接将其赋值给byte,short,char 类型的变量,int自动强转,这是个例外
(注意是赋值阶段给他一个准确的数值才能让编译器判断是否超出范围,如byte a=3;byte b = a + 4;则编译不能通过,因为运算结果是int类型,并且a是变量,即使当前a+4不超范围,编译器认为后续a变化可能导致b超出byte范围);
-
小容量向大容量转换称为自动类型转换,容量从小到大的排序为:byte < short(char) < int < long < float < double,其中 short 和 char 都占用两个字节,但是 char 可以表示更大的正整数;
-
大容量转换成小容量,称为强制类型转换,编写时必须添加“强制类型转换符”,但运行时可能出现精度损失,谨慎使用;语法:(目标类型)(待转换的数据)注意优先级
-
byte,short,char 类型混合运算时,先各自转换成 int 类型再做运算,结果是int型;
-
多种数据类型混合运算,各自先转换成容量最大的那一种再做运算,结果是容量大的类型;
运算符![](https://i-blog.csdnimg.cn/blog_migrate/068559cb2c758e01210aff64f6cb0db4.png)
正在上传…重新上传取消正在上传…重新上传取消
算数运算符
++--规则同C++:先++后++区别在于相对于整条语句的执行先后不同
int c = 100; System.out.println(c++); int d = 100; System.out.println(++d);
结果:100 101
关系运算符
比较两个字符串是否相等,不能使用“==”, 必须调用 equals 方法
逻辑运算符
逻辑运算符主要包括逻辑与(&),逻辑或(|),逻辑异或(^),短路与(&&),短路或(||)。所有逻辑运算符的特点是操作数都是布尔类型,并且最终的运算结果也是布尔类型。
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
短路与(&&)在左边的表达式结果为 false 的时候,右边的表达式则不再执行,这种现象被称为短路现象,这种机制也显得短路与比较智能一些,效率更高一些,所以在实际开发中短路与(&&)的使用率要比逻辑与高一些。但这并不是绝对的,有的时候也可能会选择使用逻辑与(&),这取决于你是否期望右边的表达式一定执行。
短路或(||)在左边的表达式结果为 true的时候,右边的表达式则不再执行
赋值运算符
=、+=、-=、*=、/=、%=
所有的扩展赋值运算符,最终都不会改变运算的结果类型
但是在使用扩展类赋值运算符的时候要谨慎,不小心就会精度损失
byte b = 10; //以下程序编译报错,编译器提示错误信息为: //Type mismatch: cannot convert from int to byte /* * 编译没有通过的原因:b 是 byte 类型,1 是 int 类型,根据之前 * 讲解的类型转换规则得知,byte 和 int 混合运算最后结果是 * int 类型,int 类型的值无法直接赋值给 byte 类型的变量 b, * 所以编译报错。 */ //b = b + 1; b += 1; //编译通过并可以正常运行 System.out.println("b = " + b); //11 //通过以上的测试得出:b = b + 1 和 b += 1 是不一样的 //那么 b += 1 等同于什么呢? /* * 实际上 java 对于扩展类的赋值运算符进行了特殊的处理, * 所有的扩展赋值运算符,最终都不会改变运算的结果类 * 型,假设前面的变量是 byte 类型,那么后面的表达式运 * 算之后的结果还是 byte 类型。所以实际上 b += 1 等同于: */ b = (byte)(b + 1);
条件运算符
条件运算符属于三目运算符,它的语法结构是:布尔表达式?表达式 1:表达式 2。它的运行原理是这样的,先判断布尔表达式的结果是 true 还是 false,如果是 true,则选择表达式 1 的结果作为整个表达式的结果,反之则选择表达式 2 的结果作为整个表达式的结果
字符串的连接运算符
实际上“+”运算符在 java 语言中有两个作用,作用一是对数字进行求和运算,作用二就是字符串连接运算,当“+”运算的时候,两边的操作数都是数字的话,一定会进行求和运算,只要其中有一个操作数是字符串类型,那么一定会进行字符串拼接运算,字符串拼接之后的结果还是字符串类型。需要注意的是,当一个表达式当中有多个“+”,并且在没有小括号的前提下,遵循自左向右的顺序依次执行
将变量放到字符串当中进行拼接:
-
在拼接的位置添加两个英文双引号;
-
在双引号中间添两个加号
-
把字符串变量放到两个加号中间
控制语句
可分为8种语句:if 语句、switch 语句、for 循环、while 循环、do..while 循环、break 语句、continue 语句、return 语句。
四大类:if 和switch 语句属于选择语句,for、while、do..while 语句属于循环语句,break 和 continue 语句属于转向语句,return 属于返回语句
if语句
注意:当分支中只有一条 java 语句话,大括号可以省略不写
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
第三种写法原理:语句依次判断正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
注意进入一个语句后执行完就走了,不会再判断
第四种写法原理:正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
switch语句
语法:正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
原理:正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
①switch 语句除了支持int 类型之外,还支持 String 类型
② switch 虽然只能探测 int 类型,但是也可以将 byte,short,char 类型放到小括号当中,因为这些类型会自动转换成 int 类型(小容量向大容量转换称为自动类型转换)。
③ switch 语句当中 case 是可以进行合并的,default 可以写在任何位置,但它的执行时机是不变的,永远都是在所有分支都没有匹配成功的时候才会执行
switch(num){ case 1: case 2: case 3: case 4: case 5: //java语句 break; }
④ switch 语句当中当某个分支匹配成功,则开始执行此分支当中的 java 语句,当遇到当前分支中的“break;”语句,switch 语句就结束了,但是如果当前分支中没有“break;”语句,则会发生 case 穿透现象,不断直接进入下一个分支执行,直到遇到“break;”为止,同C++;
注意:
for循环
同C++
读懂循环要关注最后一次循环,即满足临界条件下执行的那次循环
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
while和do while循环
两者区别: do..while 循环是 while 循环的变形,它们的区别在于 do..while 循环可以保证循环体执行次数至少为 1 次,也就是说 do..while 循环的循环体执行次数是 1~N 次,它有点儿先斩后奏的意思,而 while 循环的循环体执行次数为 0~N 次。
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
转向语句
break:1. switch语句分支语句结束
-
循环体中用来终止循环,默认情况下只能终止离它“最近”的“一层”循环。
continue:跳出当前本次循环,直接进入下一次循环
(注意该次循环中continue语句后的语句不会继续执行)
中止指定的循环:多层循环嵌套的时候,可以给每个循环设置标识,例如:first:for...、second:for...,当某个条件成立时,想终止指定循环的话,可以这样做:break first;或者 continue second;让指定的循环结束或跳出
方法
语法
定义/声明:
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
调用方法:
-
静态方法:类名.方法名(实参列表)
-
实例方法:引用/this.方法(实参列表)
类名省略:当a()方法和 b()方法在同一个类当中, a()方法执行过程中调用 b()方法,此时“类名.”可以省略不写,但如果 a()方法和 b()方法不在同一个类当中,“类名.”则不能省略。
返回值
-
当一个方法在声明的时候返回值类型不是 void 的情况下,要求方法体中必须有负责“返回数据”的语句,使用“return + 数据”语句
-
带有 return 关键字的语句只要执行,所在的方法则执行结束,所以在同一个“域”中,“return”语句后面不能写任何代码,因为它无法执行到,会编译错误
-
java 语法要求方法必须能够“百分百的保证”在结束的时候返回数据,如果在方法中有条件判断分支,则切记每种情况都要保证有返回值
-
当一个方法的返回值类型是 void 的时候,方法体当中允许出现“return;”语句(注意:不允许出现“return 值;”),这个语法的作用主要是用来终止方法的执行。如在方法执行过程中,如果满足了某个条件,则这个方法可能就没必要往下继续执行了,想终止这个方法的执行,此时执行“return;”就行了。(这个方法只能用于返回void)
方法执行过程中内存的变化
JVM内存结构图:
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
方法区:存储类信息,或者也可以理解为代码片段
栈区: 方法在执行过程中需要的内存空间在栈中分配。注意只有栈帧具有活跃权(能执行代码),其他位置只能暂停。
java 程序开始执行的时候先通过类加载器子系统找到硬盘上的字节码(class)文件,然后将其加载到 java 虚拟机的方法区当中,开始调用 main 方法,main 方法被调用的瞬间,会给 main 方法在“栈”内存中分配所属的活动空间,此时发生压栈动作,main 方法的活动空间处于栈底。只有在调用的瞬间,java 虚拟机才会在“栈内存”当中给该方法分配活动空间,此时发生压栈动作,直到这个方法执行结束的时候,这个方法在栈内存中所对应的活动空间就会释放掉,此时发生弹栈动作。由于栈的特点是先进后出,所以最先调用的方法(最先压栈)一定是最后结束的(最后弹栈)。
java 虚拟机内存管理简图:正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
方法重载
方法重载(overload)是指在同一个类中定义多个同名方法,但要求每个方法具有不同的参数的类型或参数的个数,注意与修饰符无关。调用重载方法时,Java 编译器能通过检查调用的方法的参数类型和个数选择一个恰当的方法。方法重载通常用于创建完成一组任务相似但参数的类型或参数的个数不同的方法。调用方法时通过传递给它们的不同个数和类型的实参来决定具体使用哪个方法。
重载的三个条件:
-
在同一个类当中。
-
除参数列表外和修饰符(没有影响)外其他相同。
-
参数列表不同:个数不同||类型不同
类和对象
编译方法: 类文件必须放在同个包
javac xxx.java(每个类都编译或只编译公共类都行,编译公共类时类加载器会自动找到需要的类去编译装载)
javac *.java
基本语法
常用变量
局部变量: 在方法体当中声明的变量以及方法的每个参数
成员变量: 在方法体外,类体内声明的变量,包括实例变量和静态变量
实例变量:对象被称为实例,实例变量即对象级别的变量,这些属性因对象而异,不能直接通过类去访问
静态变量:类级别的变量,这些属性是类共有共享的属性,可以直接通过类去访问
引用:保存对象内存地址的一大类数据类型,再细分就是各种类的引用,它实际上代表了这个对象,在声明时会自动识别,不用手动标识引用类型(只要是实例对象就是)。我们必须通过引用(地址)去访问堆内存
注意:引用和c++的指针很类似,本身是一大类数据类型,具体因指向而异,声明时的类就是他指向的数据类型
类的定义
类的定义:[修饰符] class 类名 {
类体 = 属性 + 方法
}
类的创建:类名 变量名=new 类名()
实例变量访问:
引用.属性名
-
一个变量可能同时具有多种性质,各种变量的生存周期没有绝对的谁比谁大或小
-
对象的缺省值为null(关键字)
-
空指针异常:空引用访问实例对象的数据时报错,如成员变量中的实例对象默认为null(无地址)
-
成员变量是对象:该变量是引用数据类型,实例化后这个实例变量实际上存储的是引用,指向该成员变量对应的实例对象
构造方法
!!!实例变量在构造方法执行的时候初始化
语法:
[修饰符列表] 类名(形式参数列表){
构造方法体;
}
-
构造方法名和类名一致。
-
构造方法用来创建对象,以及完成属性初始化操作。
-
构造方法返回值类型不需要写,写上就报错,包括 void 也不能写。
-
构造方法的返回值类型实际上是当前类的类型(引用数据类型)。执行结束之后会返回该对象在堆内存当中的内存地址。
-
一个类中可以定义多个构造方法,这些构造方法构成方法重载。
-
在构造函数中没有手动赋值,则系统还是自动给实例变量赋默认值。
-
手动定义了构造函数,系统将不会给默认构造了
封装
封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节,但可以通过该对象对外提供的接口来访问该对象。
private关键字:修饰的数据表示私有的,私有的数据只能在本类当中访问。
封装步骤:
需要被保护的属性使用 private 进行修饰,给这个私有的属性对外提供公开的 set 和 get 方法,其中 set 方法用来修改属性的值,get 方法用来读取属性的值。并且 set 和 get 方法在命名上也是有规范的,规范中要求 set 方法名是 set + 属性名(属性名首字母大写),get 方法名是 get + 属性名(属性名首字母大写)。其中 set 方法有一个参数,用来给属性赋值,set 方法没有返回值,一般在 set 方法内部编写安全控制程序,而 get 方法不需要参数,返回值类型是该属性所属类型
this和static
this:一种引用,存储在 Java 虚拟机堆内存的对象内部,this 这个引用保存了当前对象的内存地址,this 指向“当前对象”,也可以说 this 代表“当前对象”,注意只有对象有this,类没有this
-
this 可以使用在实例方法中以及构造方法中,语法格式分别为“this.”和“this(..)”。this 不能出现在带有 static 的方法当中。在实例方法中大部分情况下 this 都是可以省略的(调用成员变量或实例方法),只有当在实例方法中区分局部变量和实例变量的时候不能省略。
-
this 还有另外一种用法,使用在构造方法第一行(只能出现在第一行,这是规定,记住就行),通过当前构造方法调用本类当中其它的构造方法,其目的是为了代码复用。调用时的语法格式是:this(实际参数列表)
实例和静态:静态是类层面的(整个类共有),实例是对象层面的(每个对象独有)
static:表示“静态的”,它可以用来修饰变量、方法、代码块等,修饰的变量叫做静态变量,修饰的方法叫做静态方法,修饰的代码块叫做静态代码块。在 java语言中凡是用 static 修饰的都是类相关的,不需要创建对象,直接通过“类名”即可访问
-
静态变量:在类加载的时候初始化,存储在方法区当中,不需要创建对象,直接通过“类名”来访问。即使使用“引用”去访问,在运行的时候也和堆内存当中的对象无关,还是会通过类去访问
-
静态代码块:实际上是 java 语言为程序员准备的一个特殊的时刻,这个时刻就是类加载时刻,在类加载时执行,并且只执行一次。一个类中可以编写多个静态代码块(尽管大部分情况下只编写一个),并且静态代码块和静态变量遵循自上而下的顺序依次执行,所以有的时候放在类体当中的代码是有执行顺序的(大部分情况下类体当中的代码没有顺序要求,方法体当中的代码是有顺序要求的,方法体当中的代码必须遵守自上而下的顺序依次逐行执行),静态代码块当中的代码在 main 方法执行之前执行,
-
静态方法:非实例即静态,不需要对象参与,在类层面的方法,在实际的开发中,“工具类”当中的方法一般定义为静态方法,因为工具类就是为了方便大家的使用,将方法定义为静态方法,比较方便调用,不需要创建对象,直接使用类名就可以访问
-
在 static 方法中无法直接访问实例变量和实例方法(实例变量和实例方法是对象级别的,需要引用/this调用,类无this指针)。
注意:1. 实例方法可以访问所有成员,静态方法只能访问静态成员
-
唯一的静态类是静态内部类
继承
继承语法和注意
class 类名 extends 父类名{
类体;
}
① B类继承 A类,则称 A类为超类(superclass)、父类、基类,B类则称为子类(subclass)、派生类、扩展类。
② java 中的继承只支持单继承,不支持多继承,C++中支持多继承,这也是 java 体现简单性的一点,换句话说,java 中不允许这样写代码:class B extends A,C{ }。
③ 虽然 java 中不支持多继承,但有的时候会产生间接继承的效果,例如:class C extends B,class B extends A,也就是说,C 直接继承 B,其实 C 还间接继承 A。 (接口可以多继承,特殊)
④ java 中规定,子类继承父类,除构造方法和被 private 修饰的数据不能继承外,剩下都可以继承。实际上子类会继承父类之中的所有属性和方法,但对于所有的非私有(no private)属于显式继承(可以直接利用对象操作),而所有的私有操作属于隐式继承(间接完成)。
⑤ java 中的类没有显示的继承任何类,则默认继承 Object 类,Object 类是 java 语言提供的根类(老祖宗类),也就是说,一个对象与生俱来就有 Object 类型的所有的特征。
⑥ 继承也存在一些缺点,例如父类和子类之间的耦合度非常高,父类发生改变之后会马上影响到子类
执行顺序:父为先,类加载时先加载父类在加载子类,构造方法也是先执行父类再执行子类
super
严格来说,super 其实并不是一个引用(并不指向单独的对象),它只是一个关键字,super 代表了当前对象中那些从父类继承过来的特征。(换句话说super 其实是 this 的一部分)this指向对象整体,super只是指向继承特征那部分
-
super 和 this 都可以使用在实例方法当中。
-
super 不能使用在静态方法当中,因为 super 代表了当前对象上的父类型特征,也是属于对象级别的
-
super 也有这种用法:“super(实际参数列表);”,这种用法是通过当前的构造方法调用父类的构造方法。从本质上来说并不是创建一个“独立的父类对象”,而是为了完成当前对象的父类型特征的初始化操作。
-
当一个构造方法第一行没有显示的调用“super(实际参数列表)”的话,系统默认调用父类的无参数构造方法“super()”,自动写上super()
-
父类和子类中允许有同名的变量和方法,当父类和子类中有同名实例变量或者有同名的实例方法,则想在子类中访问父类中的实例变量或实例方法,则super 是不能省略的,其它情况都可以省略(如方法覆盖中调用父类方法)
多态
核心思想:同一个行为具有多种不同表现形式或形态的能力,主要的要求就是调用对象是可以进行灵活切换的,灵活切换的前提就是解耦合,解耦合依赖多态机制。
多态存在的三个条件分别是:
① 继承
② 方法覆盖
③ 父类型引用指向子类型对象
方法覆盖
注意和方法重载区分
当父类中继承过来的方法无法满足当前子类业务需求的时候,子类有必要将父类中继承过来的方法进行覆盖/重写。方法覆盖之后子类对象在调用的时候一定会执行覆盖之后的方法
构成覆盖的条件:
① 方法覆盖发生在具有继承关系的父子类之间,这是首要条件;
② 覆盖之后的方法与原方法具有相同的返回值类型、相同的方法名、相同的形式参数列表,只有方法体不同
注意:
① 由于覆盖之后的方法声明与原方法一模一样,建议在开发的时候采用复制粘贴的方式,不建议手写,因为手写的时候非常容易出错
② 私有的方法不能被继承,所以不能被覆盖;
③ 构造方法不能被继承,所以也不能被覆盖;
④ 覆盖之后的方法不能比原方法拥有更低的访问权限,可以更高
⑤ 覆盖之后的方法不能比原方法抛出更多的异常,可以相同或更少
⑥ 方法覆盖只是和方法有关,和属性无关;
⑦ 静态方法不存在覆盖,因为不适用于多态(不是静态方法不能覆盖,是静态方法覆盖意义不大,因为静态方法的执行只和引用的类型有关,与该引用指向的类型无关,即和多态无关(多态和对象指向有关),也就是说静态方法的“覆盖”是没有意义的,所以通常我们不谈静态方法的覆盖)
多态基础
多态(Polymorphism)属于面向对象三大特征之一,它的前提是封装形成独立体,独立体之间存在继承关系,从而产生多态机制。
在 java 中允许这样的两种语法出现,一种是向上转型(Upcasting),一种是向下转型(Downcasting),父在上子在下,向上转型是指子类型转换为父类型,又被称为自动类型转换,向下转型是指父类型转换为子类型,又被称为强制类型转换,属于数据类型转换
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
向上转型:子类型转换为父类型, java 允许一个父类型的引用指向一个子类型的对象,属于向上转换(Upcasting),或者叫做自动类型转换。意思就是声明引用指向父类,实际上指向子类,子类向父类转换
执行过程:创建一个父类引用指向子类对象,在编译阶段编译器根据引用的数据类型去对应父类文件字节码中将方法绑定到该引用上,编译通过了(编译器不会去管这个引用的指向,new的对象是运行阶段时再创建的对象),这个过程我们可以理解为“静态绑定”阶段完成了。紧接着程序开始运行,进入运行阶段,在运行的时候实际上在堆内存中 new的对象是子类,也就是说真正执行该方法的是子类,所以会自动执行子类当中的同名方法,这个过程可以称为“动态绑定”。但无论是什么时候,必须先“静态绑定”成功之后才能进入“动态绑定”阶段
向下转型:父类型转换为子类型,只有在访问子类型中特有数据的时候,需要先进行向下转型。
注意:强转的时候需要instanceof判断该实例对象是哪个子类创建的,父类向下强转后也必须是那个子类,比如动物类,实例对象指向猫,不可能强转成狗,只可以转成猫
语法同强制类型转换
instanceof关键字
instanceof 运算符的运算结果是布尔类型,可以在运行阶段动态判断引用指向的类型,左边是对象,右边是类,返回类型是Boolean类型。它的具体作用是测试左边的实例对象是否是右边类或者其的子类创建的,是,则返回true,否则返回false。
在实际开发中,一个方法需要用到向下转型,且参数不知道传入的是哪种子类,此时就需要用instanceof做判断再向下转型。
语法:(引用 instanceof 类型)
如果确实是指向该类型则返回true,否则返回false
注意:
-
在进行任何向下转型的操作之前,要使用 instanceof 进行判断,这是一个很好的编程习惯
final关键字
-
final 修饰的类不能被继承
-
final 修饰的方法不能被覆盖
-
final 修饰的变量只能赋一次值
-
final 修饰的实例变量必须显示初始化(定义实例变量时赋值,在实例代码块中或通过构造方法赋值),因为系统不会对其赋默认值(实例变量在构造方法执行时初始化),final 修饰的实例变量一般和public,static联用,称为常量,存储在方法区,类加载时初始化(本身也是静态变量),public公开也改不了值
-
如果修饰的引用,那么这个引用固定指向一个对象,也就是说这个引用不能再次赋值,但被指向的对象的内容是可以修改的
-
构造方法不能被 final 修饰,会影响 JAVA类的初始化:final 定义的静态常量调用时不会执行 java 的类初始化方法,也就是说不会执行 static 代码块等相关语句,这是由 java 虚拟机规定的。我们不需要了解的很深,有个概念就可以了
抽象类
概念:类的进一步抽象,要有层级的概念
向上抽象,向下实例,对象(现实存在的)的抽象是类,类的抽象是抽象类,在抽象类中可以定义一些子类公共的方法或属性,子类就可以直接继承使用了,而不需要重复定义
-
在 java 中采用 abstract 关键字定义的类就是抽象类,采用 abstract 关键字定义的方法就是抽象方法的方法只需在抽象类中提供声明,不需要实现,起到了一个强制的约束作用,要求子类必须实现。这样就能实现多态的机制,有了多态的机制,我们在运行期就可以动态的调用子类的方法。所以在运行期可以灵活的互换实现。
-
如果一个类中含有抽象方法,那么这个类必须定义成抽象类
-
如果这个类是抽象的,那么这个类被子类继承,抽象方法必须被重写。如果在子类中不复写该抽象方法,那么必须将此类再次声明为抽象类
-
抽象类是不能实例化(即使能实例也只是更具体的类而已),就像现实世界中人其实是抽象的,张三、李四才是具体的
-
抽象类不能被 final 修饰,两个关键字是矛盾的
-
抽象方法不能被 final 修饰,因为抽象方法就是被子类实现的
-
抽象类中可以包含方法实现,可以将一些公共的代码放到抽象类中
接口
接口我们可以看作是抽象类的一种特殊情况,即纯抽象类,只能定义抽象方法和常量
接口是一个父类,其他类实现的同时也继承了他,在调用时也是利用多态的原理
接口不能实例化,但它可以指向子类实例对象(可以设置接口数组)
-
在 java 中接口采用 interface 声明
-
接口中的方法默认都是 public abstract 的,不能更改,接口就是让其他人实现的
-
接口中的变量默认都是 public static final 类型的,不能更改,所以必须显示的初始化
-
接口不能被实例化,接口中没有构造函数的概念
-
接口之间可以继承,但接口之间不能实现
-
接口中的方法只能通过类来实现,通过implements 关键字
-
如果一个类实现了接口,那么接口中所有的方法必须实现(抽象类)
-
一类可以实现多个接口
-
如果类转换成接口类型,那么类和接口之间没有继承关系也能强制转换
接口在开发中的应用:
其意义是提供了一个接口去连接两个类,解除了两个类的耦合,实现两个类不同对象的可插拔,可以完成动态绑定,进行互换,使程序具有较高的灵活
-
采用接口明确的声明了它所能提供的服务
-
解决了 Java 单继承的问题
-
实现了可接插性(重要)
接口和抽象类的区别:
-
接口描述了方法的特征,不给出实现,一方面解决 java 的单继承问题,实现了强大的可接插性
-
抽象类提供了部分实现,抽象类是不能实例化的,抽象类的存在主要是可以把公共的代码移植到抽象类中
-
面向接口编程,而不要面向具体编程(面向抽象编程,而不要面向具体编程)
-
优先选择接口(因为继承抽象类后,由于单继承此类将无法再继承别的类,会丧失此类的灵活性)
访问控制权限
属性和方法4个都能修饰
接口和类只有public和默认能修饰
不同文件要用import导入
访问权限控制符 | 本类 | 同包的其他类 | 子类 | 其他包 |
---|---|---|---|---|
public | 可以 | 可以 | 可以 | 可以 |
protected | 可以 | 可以 | 可以 | 不可以 |
默认 | 可以 | 可以 | 不可以 | 不可以 |
private | 可以 | 不可以 | 不可以 | 不可以 |
java进阶
object类
-
Object 类是所有 Java 类的根基类
-
如果在类的声明中未使用 extends 关键字指明其基类,则默认基类为 Obj
toString方法
-
作用:返回该对象的字符串表示。通常 toString 方法会返回一个“以文本方式表示”此对象的字符串,Object 类的 toString 方法返回一个字符串,该字符串由类名加标记@和此对象哈希码的无符号十六进制表示组成,Object 类 toString 源代码如下:
getClass().getName() + '@' + Integer.toHexString(hashCode())
-
在进行 String 与其它类型数据的连接操作时,如:System.out.println(student);
它会自动调用该对象的 toString()方法。其中运用了多态,因为 println 方法没有带 Person 参数的,但是 Person 是 Object的子类,所以他会调用 println(Object x)方法,这样就产生 object 指向子类 Person ,而在 Person 中覆盖了父类 Object 的 toString 方法,运行时会动态绑定并执行Person 中的 toString 方法,所以将会按照我们的需求进行输出
equals方法
-
等号可以比较基本类型和引用类型,但等号比较的是变量储存的值,特别是比较引用类型,比较的是引用的内存地址
-
由于object类中的equals方法比较的也是两个引用的地址,所以需要我们重写
public boolean equals(Object obj) { return (this == obj); }
-
equals和==比较:==有时在判断字符串时成功的原因是Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然s1和s2的引用就是相同的。但是当两个相同字符用new在堆区创建时,==会失效
finalize方法
这也是提供给程序员的一个时机,道理同静态代码块,当垃圾收集器将要收集某个垃圾对象时将会调用 finalize,建议不要使用此方法,因为此方法的运行时间不确定,如果执行此方法出现错误,程序不会报告,仍然继续运行
垃圾回收器(Garbage Collection),也叫 GC,垃圾回收器主要有以下特点:
-
当对象不再被程序使用时,垃圾回收器将会将其回收
-
垃圾回收是在后台运行的,我们无法命令垃圾回收器马上回收资源,但是我们可以告诉他,尽快回收资源(System.gc 和 Runtime.getRuntime().gc())
-
垃圾回收器在回收某个对象的时候,首先会调用该对象的 finalize 方法
-
GC 主要针对堆内存
-
单例模式的缺点
内部类
在一个类的内部定义的类,称为内部类
内部类主要分类:
-
实例内部类(普通内部类):
-
作为外部类的一个实例属性
-
创建实例内部类,外部类的实例必须已经创建,实例内部类对象依赖外部类对象而存在
-
持有外部类的引用,内部类对象可以访问外部类对象中所有访问权限的字段,同时,外部类对象也可以通过内部类的对象引用来访问内部类中定义的所有访问权限的字段
-
不能定义 static 成员,只能定义实例成员
-
-
局部内部类:
-
局部内部类是在方法中定义的,它只能在当前方法中使用。和局部变量的作用一样,在局部内部类里面可以访问外部类对象的所有访问权限的字段,而外部类却不能访问局部内部类中定义的字段,因为局部内部类的定义只在其特定的方法体 / 代码块中有效,一旦出了这个定义域,那么其定义就失效了
-
-
局部内部类和实例内部类一致,不能包含静态成员
-
静态内部类
-
静态内部类也是作为一个外部类的静态成员而存在,即所有外部类共享这个成员,可以通过
类名.静态成员名
的形式来访问这个静态成员,同样的,创建一个类的静态内部类对象不需要依赖其外部类实例对象 -
和静态方法类似,由于其是类共享的,所以在静态内部类中也无法访问外部类的非静态成员,外部类依然可以访问静态内部类对象的所有访问权限的成员
-
-
匿名内部类:
-
常见于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法,相当于new一个匿名类的对象对接口直接实现(对某个类的继承),缺点是只能用一次
-
new 一个已经存在的类 / 抽象类,并且选择性的实现这个类中的一个或者多个非 final 的方法,这个过程会创建一个匿名内部类对象继承对应的类 / 抽象类,并且重写对应的方法。
同样的,在匿名内部类中可以使用外部类的属性,但是外部类却不能使用匿名内部类中定义的属性,因为是匿名内部类,因此在外部类中无法获取这个类的类名,也就无法得到属性信息。
语法格式 new 接口名(){
对接口的实现...
}
-
innerClassTest05.method1(new MyInterface() { public void add() { System.out.println("-------add------"); } };
Lambda表达式
数组
基本概念:一组数据的集合,从类的角度来说是一个大类,再细分就是存储各种基本数据类型或对象的类,
从对象的角度来说就是数组类的实例对象,从变量的角度来说是一个引用
(存某个类的数组类的对象/引用)
-
是一种引用数据类型(实例对象),数组的父类是object类,数组对象存储在堆区
-
数组作为对象,数组中的元素作为对象的属性,除此之外数组还包括一个成员属性 length,length 表示数组的长度
-
数组中存对象的话存的是对象的引用
数组优缺点:
-
优点:
-
每一个元素的内存地址在空间上是连续的
-
每个元素占用的内存空间相同
-
知道首地址即可通过数学公式机算某个元素的位置,直接通过内存地址定位元素,检索效率是最高的
-
-
缺点:
-
增删元素要移动后面的元素,效率较低
-
很难找到一块很大的连续内存空间
-
初始化
数组元素的类型[] 变量名称
数组元素的类型 变量名称[]
注意:在一行中也可以声明多个数组,例如:int[] a, b, c
-
知道数组具体元素时
静态初始化: 直接赋值{数组元素 1,数组元素 2,......数组元素 n}
int[] arr=new int[]{};
-
只知道数组元素个数时
动态初始化: new 数组元素的数据类型[数组元素的个数]
int[] arr = new int[3];
二维数组
当数组元素的类型是数组时就成了多维数组,[]的个数就代表了数组的维数,创建是从高维开始逐维创建
二维数组的声明格式如下:
-
数组元素的数据类[][] 变量名;
-
数组元素的数据类型 变量名[][];
初始化和一维数组同理 ,分为静态初始化和动态初始化
int [][] arr=new int[][]{ {},{},{},... };
int[][]arr=new int[5][9];
不定长二维数组
int[][] array = new int[3][]; array[0] = new int[1]; array[1] = new int[2]; array[2] = new int[3];
数组算法
算法不必精通,很多东西已经封装好了,实际开发中都用工具类,考试的时候背一下就行,不要浪费太多时间在算法上,等以后语言学得差不多了再去了解算法岗,
排序算法:
//冒泡排序:相邻数字比较,满足大小条件就交换,每轮冒出一个数 for (int i=data.length-1; i>0; i--) { for (int j=0; j<i; j++) { if (data[j] > data[j+1]) { int temp = data[j]; data[j] = data[j+1]; data[j+1] = temp; } } } //选择排序:也是每个相邻数字比较,但不交换而是标记最小的数,最后再确定是否交换,每轮只交换一个数,和冒泡排序相比交换次数减少,但比较次数仍然没有减少。 for (int i=0; i<data.length; i++) { int min = i; for (int j=i+1; j<data.length; j++) { if (data[j] < data[min]) { min = j; } } //进行位置的交换 if (min != i) { int temp = data[i]; data[i] = data[min]; data[min] = temp; } }
二分法查找:查找数组中的元素我们可以遍历数组中的所有元素,这种方式称为线性查找。线性查找适合与小型数组,大型数组效率太低。如果一个数组已经排好序,那么我们可以采用效率比较高的二分查找或叫折半查找算法。
非递归方法 public static int binarySearch1(int[] a,int e) { int begin=0; int rear=a.length-1; while(begin<=rear) { int middle=(begin+rear)/2; if(a[middle]==e)return middle+1; else if (a[middle]<e) begin=middle+1; else rear=middle-1; } return -1; } 递归方法 对于复杂一点的递归要明确函数的“意义” public static int binarySearch2(int[] a,int e,int begin,int end)//begin和end表示查找的初末位 { if(begin>end)return -1;//当全部找完没有这个数的情况 int middle=(begin+end)/2; if(a[middle]==e)return middle+1; else if(a[middle]<e) return binarySearch2(a,e,middle+1,end); else return binarySearch2(a,e,begin,middle-1); }
异常
基本概念:异常是在程序运行过程中出现的错误,异常机制是指出现异常后,程序如何处理。具体来说,异常机制提供了程序退出的安全通道。当出现错误后,程序执行的流程发生改变,程序的控制权转移到异常处理器。java 异常都是类,在异常类中会携带一些信息给我们,我们可以通过这个类把信息取出来
异常的层次结构
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
异常类型:
-
错误 对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和预防,遇到这样的错误,程序终止。Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。
-
受控异常 这种异常属于一般性异常,我们出现了这种异常必须显示的处理,不显示处理java程序将无法编译通过。编译器强制普通异常必须try…catch处理,或用throws声明继续抛给上层调用方法处理,所以普通异常也称为checked异常。
-
非受控异常 非受控异常也即是运行时异常(RuntimeException),这种系统异常可以处理也可以不处理,所以编译器不强制用try…catch处理或用throws声明,所以系统异常也称为unchecked异常。这种异常可能是由于逻辑不严谨产生的(如除0,添加元素超过容量等等),通过规范的代码可以避免产生这种异常
此种异常可以不用显示的处理,例如被0除异常,java没有要求我们一定要处理, 当出现这种异常时,肯定是程序员的问题,也就是说,健壮的程序一般不会出现这种系统异常。
try,catch,finally
try
用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
catch
catch用来捕获try语句块中发生的异常。
-
try 中包含了可能产生异常的代码,出现异常时,catch可以捕捉对应类型的异常
-
try 后面是 catch,catch 可以有一个或多个
-
当 try 中的代码出现异常时,出现异常下面的代码不会执行,马上会跳转到相应的catch 语句块中,如果没有异常不会跳转到 catch 中
getMessage()和printStackTrace()
取得异常描述信息:getMessage(),可以通过引用调用
取得异常的堆栈信息(比较适合于程序调试阶段):printStackTrace()
finally
-
不管是出现异常,还是没有出现异常,finally 里的代码都执行,finally 和 catch可以分开使用,但 finally 必须和 try 一块使用,通常在 finally 里关闭资源(注意对象的作用域)
-
只有 java 虚拟机退出不会执行 finally
-
其他任何情况下都会执行 finally,如,return,throw 语句
-
只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
语法格式:
try { //1、对可能产生异常的代码进行检视 //2、如果try代码块的某条语句产生了异常, 就立即跳转到catch子句执行, try代码块后面的代码不再执行 //3、try代码块可能会有多个受检异常需要预处理, 可以通过多个catch子句分别捕获 }catch(OneException e) { //捕获异常类型1的异常, 进行处理 //在开发阶段, 一般的处理方式要么获得异常信息, 要么打印异常栈跟踪信息(e1.printStackTrace()) //在部署后, 如果有异常, 一般把异常信息打印到日志文件中, 如logger.error(e1.getMessage()); System.out.println(e.getMessage()); }catch(TwoException e) { //捕获异常类型2的异常, 进行处理 //如果捕获的异常类型有继承关系, 应该先捕获子异常再捕获父异常 //如果没有继承关系, catch子句没有先后顺序 }finally { //不管是否产生了异常, finally子句总是会执行 //一般情况下, 会在finally子句中释放系统资源 }
下面的程序应证了三个原则: 1.方法的执行永远是从上往下的 2.在try,catch,finally结构中,finally语句一定会被执行且3. return语句永远在最后执行 private static int method1() { int a = 10; try { return a; }finally { a = 100; } } 反编译结果: private static int method1() { byte byte0 = 10; byte byte3 = byte0; //将原始值进行了保存 byte byte1 = 100; return byte3; Exception exception; exception; byte byte2 = 100; throw exception; }
方法与异常
在方法定义处采用 throws 声明异常,如果声明的异常为受控异常,那么方法调用者必须处理此异常,处理的方式有两种,捕捉或继续上报
语法:方法+throws Exception
throw new Exception();
throw关键字:手动抛出异常,thorws 是声明异常,throw是抛出异常,抛出一个异常对象
throw和throws的区别:
-
throw throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。
throw是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行throw一定是抛出了某种异常。
throw一般用于抛出自定义异常。
-
throws throws语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。
throws主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。
throws表示出现异常的一种可能性,并不一定会发生这种异常。
异常的捕获顺序:异常的截获一般按照由小到大的顺序,也就是先截获子异常,再截获父异常,将父类放到前面,会出现编译问题,因为捕获异常后,给异常对象会直接给到父类引用,子异常都不会执行到,所以再次截获子类异常没有任何意义
自定义异常:通常继承于 Exception 或 RuntimeException,到底继承那个应该看具体情况来定
class MyException extends Exception { public MyException() { //调用父类的默认构造函数 super(); } public MyException(String message) { //手动调用父类的构造方法 super(message); } }
方法覆盖与异常:
规定:子类方法不能抛出比父类方法更多(更宽泛)的异常,可以不抛或抛出父类方法异常的子异常
父类不抛异常时子类可以抛RunTimeException
泛型
概念:泛型,即“参数化类型”。类似定义方法时的形参,然后调用此方法时传递实参。参数化类型就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。 泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
作用
-
泛型能更早的发现错误,如类型转换错误,通常在运行期才会发现,如果使用泛型对集合的元素进行规定,那么在编译期将会发现错误,发现的越早,越容易调试,越容易减少成本
List arrayList = new ArrayList(); arrayList.add("aaaa"); arrayList.add(100); for(int i = 0; i< arrayList.size();i++){ String item = (String)arrayList.get(i); Log.d("泛型测试","item = " + item); }
-
使用泛型可以不用进行强制转换,可以直接取得相应的元素,如采用泛型来改善自定义比较器,或对集合中元素的直接操作
泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 //在实例化泛型类时,必须指定T的具体类型 public class Generic<T>{ //key这个成员变量的类型为T,T的类型由外部指定 private T key; public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定 this.key = key; } public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定 return key; } }
注意:果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型(自动识别类型)
集合
//ctrl
概念:集合是一个容器,可以把相同类型的“数据”用合适的数据结构存储起来
所有的集合类和集合接口都在java.util包下。
集合这块最主要掌握什么内容?
-
每个集合对象的创建(new)
-
向集合中添加元素
-
从集合中取出某个元素
-
遍历集合
主要的集合类:
-
ArrayList
-
LinkedList
-
HashSet (HashMap的key,存储在HashMap集合key的元素需要同时重写hashCode + equals)
-
TreeSet
-
HashMap
-
Properties
-
TreeMap
集合继承结构
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
Java 集合主要有 3 种重要的类型:
-
Collection:List 有序可重复;
Set 无序不可重复;
-
Map:是一个无序集合,集合中每个元素包含一个键对象,一个值对象,键对象不允许重复,值对象可以重复(类比身份证号—姓名)
Collection接口
基本操作:
Iterator接口
Iterator 称为迭代接口,通过此接口可以遍历集合中的数据,Collection中的iterator()方法可以返回一个迭代器
(增强for底层也是迭代器)
注意:
-
当集合的结构发生改变的时候要重置迭代器。所以在迭代器迭代元素的过程中不能使用集合对象的remove方法删除元素,必须使用迭代器Iterator的remove方法来删除元素,防止异常:ConcurrentModificationException
-
遍历集合后若想再次遍历,需要重置迭代器(再调用一次iterator())
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
hasNext():判断游标指向位置的下一元素是否为空
next():游标向后偏移一位,指向下一位元素并返回该元素
List
-
特点:有索引,精准操作元素;
-
元素有序,存储及取出时顺序一致;
-
元素可重复,通过.equals()比较是否重复。
-
它利用索引(index),定义了一些特殊方法:
get(int index,E e) 获取指定位置的元素;remove(int index)移除指定位置的元素;
add(int index,E e) 将元素添加到指定位置;set(int index,E e) 用元素替换指定位置的元素;
ArrayList实现类
-
数据结构:数组;
-
特点:查询快,增删慢,主要用于查询遍历数据,为最常用集合之一;
-
底层分析:数组结构是有序的元素序列,在内存中开辟一段连续的空间,在空间中存放元素,每个空间都有编号,通过编号可以快速找到相应元素,因此查询快;数组初始化时长度是固定的,要想增删元素,必须创建一个新数组,把源数组的元素复制进来,随后源数组销毁,耗时长,因此增删慢。
-
ArrayList集合调用无参构造初始为空数组,第一次扩容的容量为10,之后每次扩容为原容量1.5倍。
调用有参构造传入初始值,即数组初始容量
modCount属性作用:在使用迭代器遍历的时候,用来检查列表中的元素是否发生结构性变化(列表元素数量发生改变)了,保证在多线程环境下,迭代器能够正常遍历,主要在多线程环境下需要使用,防止一个线程正在迭代遍历,另一个线程修改了这个列表的结构。对了,ArrayList是非线程安全的,所以在遍历非线程安全的集合时(ArrayList和LinkedList),最好使用迭代器。
源码
(从无参构造器开始debug,分析使用无参构造器,即 ArrayList list = new ArrayList() )
第一步:
创建了一个空的elementData数组={},Object类型,由于Object是所有类的父类,所以能够存放很多不同类型的对象。
从这里也能得出结论:ArrayList的底层其实是数组
ArrayList初始化完成后(创建空数组),代码来到第一个for循环,准备添加数据
第二步:主要是判断是否需要扩容
valueOf 对整数进行装箱处理,将基本数据类型处理为对应的Integer对象
modCount++ 记录集合被修改的次数;此时因为只是创建的空数组,所以size为0,进入另一个add方法
modCount防止多线程操作带来的异常,源码注释的解释:
大概意思就是说:在使用迭代器遍历的时候,用来检查列表中的元素是否发生结构性变化(列表元素数量发生改变)了,保证在多线程环境下,迭代器能够正常遍历,主要在多线程环境下需要使用,防止一个线程正在迭代遍历,另一个线程修改了这个列表的结构。好好认识下这个异常:ConcurrentModificationException。对了,ArrayList是非线程安全的,所以在遍历非线程安全的集合时(ArrayList和LinkedList),最好使用迭代器。
判断,当前elementData的大小够不够,如果不够就调用grow()去扩容。如果当前ArrayList长度s小于已分配的elementData数组大小,那么就直接插入新数据,elementData[s] = e,很明显。
grow() 扩容机制,两种方法。这里的话,执行第二种,即当前size为0,此时初始扩容为10的大小。
第三步:添加数据
扩容成功,可以正常添加数据了,当前ArrayList已有数据+1
内部add方法执行完毕,来到return语句,返回true,数据添加成功,后面大同小异,第一个for循环debug完毕!
第四步:已分配的内存空间用完了,需要扩容
无参构造器第一次初始分配大小为10,第一个for循环已经用完,此时来到第二个for循环,需要进行扩容:
先是装箱,装箱完毕进入add()方法,
此时,modCount为11,第11次修改,即将进入内add方法
判断是否需要扩容,true,进入扩容函数grow()
原先分配大小为10,显然得用newLength()方法进行扩容
扩容多少的计算方法可以简述为:扩容到oldCapacity的1.5倍。计算方式是:oldCapacity除以2(底层是右移一位实现除法操作),再加上原先的大小,即原先的1.5倍。
调用Arrays.copyOf()方法,该方法作用:将原先数据复制到一个指定新的大小的数组,原先数据顺序和位置不变,多出来的空间全以null。返回给elementData,完成扩容操作。
从10扩容到15,1.5倍,没用到的空间,全部是null
另一种:指定大小构造器
与第一种不同的是:一开始创建了一个指定大小的Object数组。其余后面就跟有大小,进行扩容操作一样。
Vector实现类
-
数据结构:数组;
-
特点:线程安全,效率不高;
-
底层分析:
-
Vector集合调用无参构造初始为容量为10的数组,每次扩容为原容量2倍。调用有参构造传入初始值,即数组初始容量
-
特有方法:
源码:
上面有Vector的有参构造和无参构造,无参初试默认容量为10,注意Vector在有参构造时,能够指定扩容大小。
第一张图中的newLength()方法中的三元运算符注意一下,如果我们制定了扩容大小的话,在第二张图里面就能够清晰得看到每次扩容大小等于指定大小加上原先的大小,若没有指定大小,则是加上原来的大小,即两倍。
LinkedList实现类
-
数据结构:双向链表;
-
特点:查询慢,增删快;
-
底层分析:链表分为单向和双向,就是一条链子和两条链子的区别;多出的那条链子记录了元素的顺序,因此单向链表结构无序,双向链表结构有序;链表结构没有索引,因此查询慢;链表的增删只需在原有的基础上连上链子或切断链子,因此增删快。
-
特有方法:getFirst() 返回开头元素; getLast() 返回结尾元素;
pop() 从所在堆栈中获取一个元素; push(E e) 将元素推入所在堆栈;
addFirst(E e) 添加元素到开头,头插; addLast(E e) 添加元素到结尾,尾插;
总结
List知识点
import java.util.ArrayList; import java.util.Iterator; public class ListText { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); // LinkedList<String> list = new LinkedList<String>(); list.add("zhangsan"); list.add("lisi"); list.add("wangwu"); for(int i = 1;i<list.size();i++) { System.out.println(list.get(i)); } for(String s : list) { System.out.println(s); } Iterator<String> it = list.iterator(); while(it.hasNext()) { System.out.println(it.next()); } for(Iterator<String> it1 = list.iterator();it1.hasNext();){ System.out.println(it1.next()); } } }
Set
-
特点:元素不可重复;
-
元素无序,存储及取出时顺序不一致;
-
没有索引,因此不能使用普通For循环遍历;
-
Set与Collection 接口中的方法基本一致,没有进行功能上的扩充;
HashSet
-
数据结构:哈希表(数组+单向链表/红黑树)
-
哈希表:(邻接表)一种数据结构,数组+单向链表实现,哈希表能够提供快速存取操作。哈希表是基于数组的,所以也存在缺点,数组一旦创建将不能扩展。
-
特点:查询快,元素无序,元素不可重复,没有索引;
-
扩容机制:第一次扩容大小为16,属性DEFAULT_LOAD_FACTOR = 0.75为负载因子,用于计算table数组的临界值newThr,作用是一旦哈希表的元素个数到达临界值后,就对table表进行按两倍扩容,避免大量数据导致阻塞
树化机制:对某条链表添加元素时,如果元素个数到达了8个且整个table表的大小达到了64,就会对这条链表进行树化(红黑树),如果table表大小没有达到64,再对链表添加元素会对table表进行按两倍扩容(table数组表)
-
底层分析:HashSet实际上是HashMap的key部分
注意:可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
HashSet采用哈希算法,底层用数组存储数据。默认初始化容量16,加载因子0.75。
具体实现唯一性的比较过程:通过hashCode()和equal()方法。Object类中的hashCode()的方法是所有子类都会继承这个方法,这个方法会用Hash算法算出一个Hash(哈希)码值返回。key通过hashCode()得到哈希值,再通过hash()拿去得出新的hash值,从而找到在table中的索引位置,即:新hash值的计算,并不是直接返回的key.hashCode(),而是将hashCode()值与自身的高16位进行异或运算。HashSet会将要存放对象k的Hash值去和数组长度取模,这个模就是对象要存放在数组中的位置(数组下标),如果该位置为空会直接添加进去,如果该位置已经有链表,则拿k与链表上的每一个元素进行equals()方法,如果true,那么就是同一个对象,无需存储;如果都是false,则将k加到链表尾,这样就保证了元素的唯一性。Hash算法是一种散列算法。
覆盖hashCode()方法的原则: 1、一定要让那些我们认为相同的对象返回相同的hashCode值 2、尽量让那些我们认为不同的对象返回不同的hashCode值,否则,就会增加冲突的概率。 3、尽量的让hashCode值散列开(两值用异或运算可使结果的范围更广)
LinkedHashSet
-
数据结构:哈希表(数组+双向链表/红黑树),当链表长度超过阈值(8)时,链表将转换为红黑树。每个结点Entry比Node多了before和after(保留next)
-
特点:查询快,元素有序,元素不可重复,没有索引;
-
底层分析:作为HashSet的子类,只是比它多了一条链表,这条链表用来记录元素顺序,因此LinkedHashSet其中的元素有序。
TreeSet
-
数据结构:红黑树
-
特点:查询快,元素有序,元素不可重复,没有索引;
-
底层分析:TreeSet实现了继承于Set接口的SortedSet接口 ,它支持两种排序方法,自然排序和定制排序,自然排序的意思就是放入元素“a”,“b”,a会自然地排在b前面,其中还有几个特有方法。
first() 返回第一个元素; last() 返回最后一个元素;comparator() 返回排序比较器; 有序性:根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法
小结:Set具有与Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只是行为不同。(这是继承与多态思想的典型应用:表现不同的行为。)Set不保存重复的元素。 Set 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。
import java.util.*; public class SetText { public static void main(String[] args) { Set<Student> students = new TreeSet<Student>(new MyComparator());//此处可以使用内部类 Student s1=new Student(1,"zhangsan"); Student s2=new Student(4,"lisi"); Student s3=new Student(2,"wangwu"); students.add(s1); students.add(s2); students.add(s3); for(Student s: students) { System.out.println(s); } Iterator<Student> it=students.iterator(); while(it.hasNext()) { System.out.println(it.next()); } } } class Student implements Comparable<Student> { private int no; private String name; public Student(int no, String name) { this.no = no; this.name = name; } @Override public String toString() { return "Student{" + "no=" + no + ", name='" + name + '\'' + '}'; } public int getNo() { return no; } public String getName() { return name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return no == student.no && Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(no, name); } @Override public int compareTo(Student o) { return this.no - o.no; } } class MyComparator implements Comparator<Student>{ @Override public int compare(Student o1, Student o2) { return o1.getNo()-o2.getNo(); } }
List和Set:
-
List,Set都是继承自Collection接口,Map则不是
-
List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
-
Set和List优缺点: Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。 List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
-
ArrayList与LinkedList的优缺点和适用场景 Arraylist: 优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。 缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。
LinkedList: 优点:LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景 缺点:因为LinkedList要移动指针,所以查询操作性能比较低。 适用场景分析: 当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList。
Map接口
特点:元素包含两个值(key,value)即键值对, key不允许重复,value可以重复, key与value是一一对应的。元素无序;
Map接口是双列集合的最顶层接口,定义了一些通用的方法。
put(key , value) 添加元素; remove(key) 删除key对应元素;
containsKey(key) 判断是否存在key对应元素;get(key) 获取key对应元素;
KeySet() 获取所有的key,存到Set集合中;entrySet() 获取所有的元素,存到Set集合中;
ps:Map集合必须保证保证key唯一,作为key,必须重写hashCode方法和equals方法,以保证key唯一,加入元素的key重复则覆盖value
HashMap
数据结构:JDK1.8之前:哈希表(数组+单向链表);JDK1.8之后:哈希表(数组+单向链表+红黑树),当链表长度超过阈值(8)时,链表将转换为红黑树。
特点:查询快,元素无序,key不允许重复但可以为null,value可以重复。
底层分析:和HashSet底层相类似,不赘述。
Hashtable
数据结构:哈希表
特点:查询快,元素无序,key不允许重复并且不可以为null,value可以重复。
底层分析:HashTable和Vector一样是古老的集合,有遗留缺陷,在JDK1.2之后 被更先进的集合取代了;HashTable是线程安全的,速度慢,HashMap是线程不安全的,速度快;
ps:Hashtable的子类Properties现在依然活跃,Properties集合是一个唯一和IO流结合的集合。
遍历Map的value三种方法
public static void main(String[] args) { Map<Integer,String> m=new HashMap<>(); m.put(3,"zhangsan"); m.put(4,"lisi"); m.put(5,"wangwu"); m.put(6,"zhaoliu"); //1.获取哈希表的key Set<Integer> s=m.keySet(); Iterator<Integer> it=s.iterator(); for(Integer i:s){ System.out.println(m.get(i)); } while(it.hasNext()) { System.out.println(m.get(it.next())); } //2.获取values Collection<String> values=m.values(); for(String v:values){ System.out.println(v); } Iterator<String> it1=values.iterator(); while (it1.hasNext()) { String value= it1.next(); System.out.println(value); } //3.获取EntrySet Set<Map.Entry<Integer,String>> s1=m.entrySet(); for(Map.Entry<Integer,String> i:s1){ System.out.println(i.getKey()+"--->"+i.getValue()); } }
Properties类
Properties
可保存在流中或从流中加载。属性列表中每个键及其对应值都是一个字符串。
setProperty:正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消 getProperty:
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
从properties文件直接读取键值对
import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.Properties; public static void main(String[] args) throws Exception { InputStream reader = Thread.currentThread().getContextClassLoader().getResourceAsStream("temp.properties"); Properties pro = new Properties(); pro.load(reader);//加载,传入Reader或InputStream pro.list(System.out);//传入PrintStrem或PrintWriter指定输出全部键值对 String username = pro.getProperty("username");//获取指定数据 System.out.println(username); String password = pro.getProperty("password"); System.out.println(password); ResourceBundle bundle = ResourceBundle.getBundle("temp"); String username1 = bundle.getString("username"); System.out.println(username1); pro.setProperty("key", String.valueOf(000));//设置键值对,有则重置,无则新建 pro.store(new FileOutputStream("src\\temp.properties"),null); }
TreeMap
数据结构:红黑树
特点:查询快,元素有序,key不允许重复并且不可以为null,value可以重复。
底层分析:和TreeSet底层相类似,不赘述。
小结
-
HashMap 非线程安全
-
HashMap:基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写】hashCode()和equals()],为了优化HashMap空间的使用,您可以调优初始容量和负载因子。
-
TreeMap:非线程安全基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
适用场景分析:
-
HashMap和HashTable:HashMap去掉了HashTable的contains方法,但是加上了containsValue()和containsKey()方法。HashTable同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允许空键值,而HashTable不允许。
-
HashMap:适用于Map中插入、删除和定位元素。 Treemap:适用于按自然顺序或自定义顺序遍历键(key)。
线程安全集合类与非线程安全集合类
-
LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;
-
HashMap是非线程安全的,HashTable是线程安全的;
-
StringBuilder是非线程安全的,StringBuffer是线程安全的。
数据结构
-
ArrayXxx:底层数据结构是数组,查询快,增删慢
-
LinkedXxx:底层数据结构是链表,查询慢,增删快
-
HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
-
TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序
import java.util.HashMap; import java.util.Map; import java.util.Set; public class MapText { public static void main(String[] args) { Map<Integer, String> map = new HashMap<Integer, String>(); map.put(1,"zhangsan"); map.put(2,"lisi"); map.put(3,"wangwu"); Set<Integer> s1=map.keySet(); for(Integer i:s1){ System.out.println(map.get(i)); } Set<Map.Entry<Integer, String>> s2 = map.entrySet(); for(Map.Entry<Integer, String> node:s2){ System.out.println(node.getKey()+node.getValue()); } } }
遗留类和取代类
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
Comparable 接口
定义比较对象大小的规则
三种实现方式:
-
直接让比较类实现Comparable<> 接口的compareTo方法
-
自定义比较器类实现Comparator<T>接口的compare(T o1, T o2) 方法
-
利用某些集合的构造方法(传入比较器)和匿名内部类
代码示例:
Set set = new TreeSet(new Comparator() {//set的构造方法可以传入一个比较器 public int compare(Object o1, Object o2) { if (!(o1 instanceof Person)) { throw new IllegalArgumentException("非法参数,o1=" + o1); } if (!(o2 instanceof Person)) { throw new IllegalArgumentException("非法参数,o2=" + o2); } Person p1 = (Person)o1; Person p2 = (Person)o2; return p1.age - p2.age; } });
Comparator 和 Comparable 的区别
-
Comparable 是默认的比较接口,Comparable 和需要比较的对象紧密结合到一起了,一个类实现了 Camparable 接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用 sort 方法排序。
-
Comparator 可以分离比较规则,所以它更具灵活性,Comparator 可以看成一种算法的实现,将算法和数据分离,Comparator 也可以在下面两种环境下使用:
-
类没有考虑到比较问题而没有实现 Comparable,可以通过 Comparator 来实现排序而不必改变对象本身
-
可以使用多种排序标准,比如升序、降序等
-
Collections 工具类
-
synchronizedList方法得到线程安全的集合
Collections.synchronizedList(list);
-
sort方法(要求集合中元素实现Comparable接口。)
IO流
基本概念
流(stream):是一个抽象的概念,是指一连串的数据(字符或字节,电信号),是以先进先出的方式发送信息的通道。
当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。
流实现java.io.Closeable接口和java.io.Flushable接口
java.io.Closeable接口:都是可关闭的,都有close()方法。流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费(占用)很多资源。养成好习惯,用完流一定要关闭。
java.io.Flushable接口:都是可刷新的,都有flush()方法。养成一个好习惯,输出流在最终输出之后,一定要记得flush()刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道!)刷新的作用就是清空管道。注意:如果没有flush()可能会导致丢失数据。
流的特点:
-
先进先出:最先写入输出流的数据最先被输入流读取到。
-
顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中的数据。(RandomAccessFile除外)
-
只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流
包装流和字节流
节点流:针对某种数据类型,直接操作某个数据源的流类,比如FileInputStream
包装流:对一个已存在的流的链接和封装,通过对数据进行处理为程序提供功能强大、灵活的读写功能,且可以包装多种类型的流类,能消除不同结点流的实现差异,忽略具体的操作对象,不会直接与数据源相连,例如BufferedInputStream(缓冲字节流)
注意:封装流是对节点流的封装,最终的数据处理还是由节点流完成的。对流的开启和关闭只需要对包装流操作就够了,处理流和节点流应用了Java的装饰者设计模式。
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
缓冲流
包装流的一种
原理:程序与磁盘的交互相对于运算是很慢的,容易成为程序的性能瓶颈。减少程序与磁盘的交互,是提升程序效率一种有效手段。缓冲流,就应用这种思路:普通流每次读写一个字节,而缓冲流在内存中设置一个缓存区,缓冲区先存储足够的待操作数据后,再与内存或磁盘进行交互。这样,在总数据量不变的情况下,通过提高每次交互的数据量,减少了交互次数。
比如在搬砖的时候,从出发点(磁盘)到目的地(cpu)一块一块地运(每次运输都是一次交互)装车(运算),这样一次交互处理一块砖(数据)是很低效的。我们可以使用一个小推车(内存中的缓冲区),先把足够多的砖装到小推车上,再把小推车推到目的地装车,一次交互处理很多砖,小推车的存在,在砖块数不变的情况下,减少了我们运输次数
缓冲流为高效率而设计的,真正的读写操作还是靠节点流。
流的分类
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
以程序为“相对坐标系”,写为output,读为input
Writer,Reader,InputSteam,OutputStream为抽象基类,流的类都是这4个类派生出来的
-
文件专属: java.io.FileInputStream(掌握) java.io.FileOutputStream(掌握) java.io.FileReader java.io.FileWriter
-
转换流:(将字节流转换成字符流) java.io.InputStreamReader java.io.OutputStreamWriter
-
缓冲流专属: java.io.BufferedReader java.io.BufferedWriter java.io.BufferedInputStream java.io.BufferedOutputStream
-
数据流专属: java.io.DataInputStream java.io.DataOutputStream
-
标准输出流: java.io.PrintWriter java.io.PrintStream(掌握)改变输出方向,日志框架
-
对象专属流: java.io.ObjectInputStream(掌握) java.io.ObjectOutputStream(掌握)
字节流
-
字节流可以处理一切文件,而字符流只能处理纯文本文件。
-
字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。
字节输入输出流类图
注:ObjectInputSteam是包装流
概述
-
InputStream:InputStream是所有字节输入流的抽象基类,前面说过抽象类不能被实例化,实际上是作为模板而存在的,为所有实现类定义了处理输入流的方法。
-
FileInputSream:文件输入流,一个非常重要的字节输入流,用于对文件进行读取操作。
-
PipedInputStream:管道字节输入流,能实现多线程间的管道通信。
-
ByteArrayInputStream:字节数组输入流,将内存缓冲区中的字节数组转化为输入流,从字节数组(byte[])中进行以字节为单位 读出 byte型数据
-
ObjectInputStream:对象输入流,用来提供对基本数据或对象的持久存储。通俗点说,也就是能直接传输对象,通常应用在反序列化中。它也是一种处理流,构造器的入参是一个InputStream的实例对象
-
FilterInputStream:装饰者类,具体的装饰者继承该类,这些类都是处理类,作用是对节点类进行封装,实现一些特殊功能。
-
DataInputStream:数据输入流,它是用来装饰其它输入流,作用是“允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型”。
-
BufferedInputStream:缓冲流,对节点流进行装饰,内部会有一个缓存区,用来存放字节,每次都是将缓存区存满然后发送,而不是一个字节或两个字节这样发送,效率更高。
-
-
-
OutputStream类继承关系与InputStream类似
-
ByteArrayOutputStream类是在创建它的实例时,程序内部创建了一个byte型的数组缓冲区,ByteArrayOutputStream将捕获的数据,转换成字节数组向数组缓冲区中 写入 数据。
注意:标准输出流指向可以用System.setout( new PrintStream())改变)
-
FileInputStream
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
-
从文件系统中的某个文件中获得输入字节。哪些文件可用取决于主机环境。
-
用于读取诸如图像数据之类的原始字节流。
注意:read()读取单个字节,返回的该字节的数据
read(byte[] b)返回的是新读入字节数组的字节数
FileOutputStream
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
(1). 文件输出流是用于将数据写入 File
或 FileDescriptor
的输出流。文件是否可用或能否可以被创建取决于基础平台。特别是某些平台一次只允许一个 FileOutputStream
(或其他文件写入对象)打开文件进行写入。在这种情况下,如果所涉及的文件已经打开,则此类中的构造方法将失败。
(2) .用于写入诸如图像数据之类的原始字节的流。
注意:每次写完数据都要用flush()刷新此输出流并强制写出任何缓冲的输出字节,或close()关闭此输出流并释放与此流关联的所有系统资源。(源码中flush和close都会调用writeBytes()方法,这才是真正干活的地方,写入数据)
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
文件复制
import java.io.*; import java.util.*; public class text { public static void main(String[] args) { FileInputStream fis=null; FileOutputStream fos=null; try { byte[] bytes=new byte[4];//类似缓冲区,一次读入小于等于4个字节,没有数据就返回-1 //字符流改成char[] fis = new FileInputStream("temp.txt"); fos = new FileOutputStream("KXXindy.txt"); int readCount=0; while((readCount=fis.read(bytes))!=-1){ System.out.print(new String(bytes,0,readCount)); fos.write(bytes,0,readCount); } fos.flush();//刷新 } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } }
字节缓冲流
BufferedInputStream
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
BufferedInputStream
为另一个输入流添加一些功能,即缓冲输入以及支持 mark
和 reset
方法的能力。在创建 BufferedInputStream
时,会创建一个内部缓冲区数组。在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。mark
操作记录输入流中的某个点,reset
操作使得在从包含的输入流中获取新字节之前,再次读取自最后一次 mark
操作后读取的所有字节。
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消 BufferedInputStream 内部的节点流属性来自FilterInputSrtream
BufferedOutputStream
该类实现缓冲的输出流。通过设置这种输出流,应用程序就可以将各个字节写入底层输出流中,而不必针对每次字节写入调用底层系统。
import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class CopyImg { public static void main(String[] args) throws IOException { BufferedInputStream bfin=new BufferedInputStream(new FileInputStream("C:\\Users\\Administrator\\Desktop\\Img.jpg")); BufferedOutputStream bfout=new BufferedOutputStream(new FileOutputStream("C:\\Users\\Administrator\\Desktop\\ImgCopybuff.jpg")); int len=0; byte[] buff=new byte[1024]; while((len=bfin.read(buff))!=-1) { bfout.write(buff, 0, len); } bfin.close(); bfout.close(); } }
对象流(序列化和反序列化)
对象流可以将 Java 对象转换成二进制写入磁盘,这个过程通常叫做序列化,并且还可以从磁盘读出完整的 Java 对象,而这个过程叫做反序列化。对象流主要包括:ObjectInputStream 和 ObjectOutputStream
如果实现序列化该类必须实现序列化接口 java.io.Serializable,该接口没有任何方法,该接口只是一种标记接口,标记这个类是可以序列化的
序列化
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("")); oos.writeObject(new Serializable(){});
反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("")); //反序列化 Person person = (Person)ois.readObject();
transient: 修饰的属性在序列化时会忽略(null)
注意:
-
读写顺序要一致
-
要求实现序列化或反序列化的对象需要实现Serializable
-
注意版本号
-
类中static或transient修饰的成员不参与序列化
-
序列化对象时要求里面属性的类型也需要实现序列化
-
序列化具备可继承性,如果某类已经实现了序列化,则它的所有子类也默认实现了序列化
public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("")); oos.writeObject(new Dog("旺财",4)); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("")); Object o= ois.readObject(); Dog dog=(Dog)o; System.out.println(dog.getName()); }
序列化版本号
一个类对应一个序列化版本化,也是用来区分类的方式,在修改类的时候JVM会重新给这个类一个序列化
在反序列化时会检查接收的对象序列化版本号是否和反序列化的类对象的序列化版本号是否相同,相同才能反序列化成功,通常我们会手动定义一个常量序列化版本号,方便后续修改类
标准流和打印流
标准输入流
System.in实际上是System的InputStream类型属性,运行类型是BufferedInputSream类型,默认设备键盘
public static final InputStream in = null;
System.setOut可以改变打印流的输出位置
打印流
PrintStream
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
PrintWriter
直接继承于Wirter
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
标准输出流
System.out实际上是PrintStream类型,默认输出显示器
public static final PrintStream out = null;
标准输出流的应用
日志记录工具类 LogUtils
import java.io.*; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Scanner; public class LogUtils { public static void log(String msg){ try { //创建打印流 PrintStream out = new PrintStream(new FileOutputStream("temp.txt"),true); //标准输出流处理一个文件输出流 System.setOut(out);//修改指向 Date nowTime=new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss"); String strTime = sdf.format(nowTime); System.out.println(strTime+":"+msg); } catch (FileNotFoundException e) { e.printStackTrace(); } } }
转换流
InputStreamReader、OutputSteamWriter
作用:将字节流包装(转换)成字符流(本身就是字符流类,可以传入构造器)
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
字符流
概述
底层还是字节流
-
InputStreamReader:从字节流到字符流的桥梁(InputStreamReader构造器入参是FileInputStream的实例对象),它读取字节并使用指定的字符集将其解码为字符。它使用的字符集可以通过名称指定,也可以显式给定,或者可以接受平台的默认字符集。
-
BufferedReader:从字符输入流中读取文本,设置一个缓冲区来提高效率。BufferedReader是对InputStreamReader的封装,前者构造器的入参就是后者的一个实例对象。
-
FileReader:用于读取字符文件的便利类,new FileReader(File file)等同于new InputStreamReader(new FileInputStream(file, true),"UTF-8"),但FileReader不能指定字符编码和默认字节缓冲区大小。
-
PipedReader :管道字符输入流。实现多线程间的管道通信。
-
CharArrayReader:从Char数组中读取数据的介质流。
-
StringReader :从String中读取数据的介质流。
字符输出流
Writer与Reader结构类似,方向相反,不再赘述。唯一有区别的是,Writer有子类PrintWriter。
FileReader
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
注意:read()和read(byte[] b)返回值不同
FileWriter
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
注意:flush()和close()
字符缓冲流
BufferedReader
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
(1)从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
(2)可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。
(3)通常,Reader 所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。因此,建议用 BufferedReader 包装所有其 read() 操作可能开销很高的 Reader(如 FileReader 和 InputStreamReader)。例如,
BufferedReader in = new BufferedReader(new FileReader("foo.in"));
(4)将缓冲指定文件的输入。如果没有缓冲,则每次调用 read() 或 readLine() 都会导致从文件中读取字节,并将其转换为字符后返回,而这是极其低效的。
注意:readLine()读不到时返回空
BufferedWriter
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
(1)将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。
(2)可以指定缓冲区的大小,或者接受默认的大小。在大多数情况下,默认值就足够大了。
(3)该类提供了 newLine() 方法,它使用平台自己的行分隔符概念,此概念由系统属性 line.separator
定义。并非所有平台都使用新行符 ('\n') 来终止各行。因此调用此方法来终止每个输出行要优于直接写入新行符。
(4)通常 Writer 将其输出立即发送到底层字符或字节流。除非要求提示输出,否则建议用 BufferedWriter 包装所有其 write() 操作可能开销很高的 Writer(如 FileWriters 和 OutputStreamWriters)。例如,
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));
(5)缓冲 PrintWriter 对文件的输出。如果没有缓冲,则每次调用 print() 方法会导致将字符转换为字节,然后立即写入到文件,而这是极其低效的。
文件复制(字符缓冲流)
import java.io.*; public class BufferedText { public static void main(String[] args) { BufferedReader br=null; BufferedWriter bw=null; try { br = new BufferedReader(new FileReader("ab.txt")); bw=new BufferedWriter(new FileWriter("KXXindy.txt")); String s=null; while((s=br.readLine())!=null) { bw.write(s); bw.newLine(); } bw.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { try { br.close(); } catch (IOException e) { e.printStackTrace(); } try { bw.close(); } catch (IOException e) { e.printStackTrace(); } } } }
File类
File类是用来操作文件(目录也当作文件)的类,但它不能操作文件中的数据。
public class File extends Object implements Serializable, Comparable\<File>
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
File类实现了Serializable、 Comparable<File>,说明它是支持序列化和排序的。
文件基本操作
-
创建新文件 //方法1:File file = new File(String pathName); //方法2:File file = new File(File parent,String child) //方法2:File file = new File(String parent,String child) String path="e:\\news2.txt"; File file = new File(path);//只是在内存中创建一个对象 file.createNewFile();//调用createNewFile()后才会真正在键盘创建一个文件
-
文件绝对路径 file.getAbsolutePath()
-
文件父级目录 file.getParent()
-
文件大小(字节) file.length()
-
文件是否存在 file.exists()
-
是不是一个文件 file.isFile()
-
是不是一个目录 file.isDirectory()
-
删除文件 file.delete()
-
创建一级目录: file.mkdir()
-
创建多级目录: file.mkdirs()
文件夹拷贝(绝对路径)
import java.io.*; import java.util.*; public class text { public static void main(String[] args) { File srcFile = new File("D:\\SteamLibrary"); File destFile = new File("E:\\"); copyDir(srcFile,destFile); } public static void copyDir(File srcFile, File destFile){//destFile是要移动到的根目录 if(srcFile.isFile()){ FileInputStream in=null; FileOutputStream out=null; try { in=new FileInputStream(srcFile); String path=(destFile.getAbsolutePath().endsWith("\\")?destFile.getAbsolutePath():destFile.getAbsolutePath()+"\\")+srcFile.getAbsolutePath().substring(3); out=new FileOutputStream(path); byte[] bytes=new byte[1024*1024]; int readCount=0; while((readCount=in.read(bytes))!=-1){ out.write(bytes, 0, readCount); } out.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { if(out!=null){ try { out.close(); } catch (IOException e) { e.printStackTrace(); } } if(in!=null){ try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } return; } File[] files = srcFile.listFiles(); for(File file : files){ if(file.isDirectory()){ String srcDir = file.getAbsolutePath(); String destDir = (destFile.getAbsolutePath().endsWith("\\")?destFile.getAbsolutePath():destFile.getAbsolutePath()+"\\")+srcDir.substring(3); File newFile = new File(destDir); if(!newFile.exists()){ newFile.mkdirs(); } } copyDir(file, destFile); } } }
线程
基本概念
进程和线程
-
一个进程是一个应用程序,有独立的内存空间(独立资源)
-
线程指进程中的一个执行场景,也就是执行流程,一个进程可以启动多个线程。同一个进程中的线程共享其进程中的内存和资源(线程和线程之间栈内存独立,堆内存和方法区内存共享,一个线程一个栈)
计算机引入多进程的作用:提高 CPU 的使用率。
实现多线程的三种方法
-
继承Thread类
-
Thread 类中创建线程最重要的两个方法为:
public void run()
public void start()
-
采用 Thread 类创建线程,用户只需要继承 Thread,覆盖 Thread 中的 run 方法,父类 Thread 中 的 run 方法没有抛出异常,那么子类也不能抛出异常,最后采用 start 启动线程即可,会自动调用线程对象的run方法
-
实现Runnable接口+Thread(Runnable taget)构造Thread对象(可以使用匿名内部类实现Runnable接口)
-
其实 Thread 对象本身就实现了 Runnable 接口,但一般建议直接使用 Runnable 接口来写多线程程序,因为接口会比类带来更多的好处
-
实现Callable接口(可以获得线程的执行结果)
import java.util.Scanner; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class Threadtext { public static void main(String[] args) { MyRunnable m=new MyRunnable(); Thread t=new Thread(m); t.start(); MyThread t1=new MyThread(); t1.start(); Thread t2=new Thread(new Runnable() { @Override public void run() { for(int i=0;i<100;i++){ System.out.println("分支线程3--->"+i); } } }); t2.start(); MyCallable myCallable=new MyCallable(); FutureTask task=new FutureTask(myCallable); Thread t3=new Thread(task); t3.start(); try { Object o=task.get();//要获得分支线程的返回结构后该线程才会继续执行 System.out.println(o); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } for(int i=0;i<100;i++){ System.out.println("主线程--->"+i); } } } class MyThread extends Thread{ public void run() { for (int i=0;i<100;i++){ System.out.println("分支线程1--->"+i); } } } class MyRunnable implements Runnable { public void run() { for (int i=0;i<100;i++){ System.out.println("分支线程2--->"+i); } } } class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("call method begin"); Thread.sleep(1000); System.out.println("call method end"); return 100+200; } }
线程的调度与控制
通常我们的计算机只有一个 CPU,CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。在单 CPU 的机器上线程不是并行运行的,只有在多个 CPU 上线程才可以并行运行。Java 虚拟机要负责线程的调度,取得 CPU的使用权,目前有两种调度模型:分时调度模型和抢占式调度模型,Java 使用抢占式调度模型。
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先级高的线程获取 CPU 的时间片相对多一些,如果线程的优先级相同,那么会随机选择一个
线程优先级: MAX_PRIORITY( 最高级 );MIN_PRIORITY (最低级)NOM_PRIORITY(标准)默认
必须在启动前设置优先级,调用方法:t1.setPriority(Thread.MAX_PRIORITY)
线程睡眠
Thread.sleep(long millis) ,静态方法,在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。当一个线程遇到 sleep 的时候,就会睡眠,进入到阻塞状态,放弃 CPU,腾出 cpu 时间片,给其他线程用,所以在开发中通常我们会这样做,使其他的线程能够取得 CPU 时间片,当睡眠时间到达了,线程会进入可运行状态,得到 CPU 时间片继续执行,如果线程在睡眠状态被中断了,将会抛出 IterruptedException
Thread.yield()
它与 sleep()类似,只是不能由用户指定暂停多长时间,并且 yield()方法只能让同优先级的线程有执行的机会
t.join()
当前线程可以调用另一个线程的 join 方法,调用后当前线程会被阻塞不再执行,直到被调用的线程执行完毕,当前线程才会执行
interrupt()(中断)
如果我们的线程正在睡眠,可以采用 interrupt 进行中断
线程安全问题
-
多线程并发
-
有共享数据
-
对共享数据有修改行为
同步线程:多个线程操作一个对象时,为了保证安全需要排队执行
sychronized关键字
-
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
-
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
-
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
注意:一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法或代码块,但是其他线程还是可以访问该实例对象的其他非synchronized方法或代码块,执行完synchronized自动归还对象锁
线程死锁现象
public class DeadThread { public static void main(String[] args) { Object o1=new Object(); Object o2=new Object(); Thread1 t1=new Thread1(o1,o2); Thread2 t2=new Thread2(o1,o2); t1.setName("t1"); t2.setName("t2"); t1.start(); t2.start(); } } class Thread1 extends Thread { Object o1; Object o2; Thread1(Object o1, Object o2){ this.o1 = o1; this.o2 = o2; } public void run() { synchronized (o1) { System.out.println(Thread.currentThread().getName()); try { Thread.sleep(10*2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2) { } } } } class Thread2 extends Thread { Object o1; Object o2; Thread2(Object o1, Object o2) { this.o1 = o1; this.o2 = o2; } public void run() { synchronized (o2) { System.out.println(Thread.currentThread().getName()); synchronized (o1) { } } } }
守护线程
使用t1.setDaemon(true)方法。从线程分类上可以分为:用户线程(以上讲的都是用户线程),另一个是守护线程。守护线程是这样的,所有的用户线程结束生命周期,守护线程才会结束生命周期,只要有一个用户线程存在,那么守护线程就不会结束,例如 java 中著名的垃圾回收器就是一个守护线程,只有应用程序中所有的线程结束,它才会结束
Timer 定时器
定义一个类继承TimerTask实现run方法
使用Timer类的对象的schedule()方法
public class TimerText { public static void main(String[] args) { try { Timer timer=new Timer(); Date firstTime; firstTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2021-11-13 16:50:00"); timer.schedule(new MyTimerTask(),firstTime,1000); } catch (ParseException e) { e.printStackTrace(); } System.out.println("over"); } } class MyTimerTask extends TimerTask { public void run() { Date now = new Date(); SimpleDateFormat s=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(s.format(now.getTime())); } }
wait()和notify()
wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。他们建立在线程同步的基础上。
wait:让正在o对象上活动的线程进入等待状态,直到被唤醒为止,o.wait();方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态。线程a在wait后被线程b notify唤醒之后线程a会继续执行wait之后的代码
notify:唤醒正在o对象上等待的线程。还有一个notifyAll()方法,唤醒o对象上处于等待的所有线程。
-
生产者和消费者模型
import java.util.ArrayList; import java.util.List; public class CP { public static void main(String[] args) { Storage storage = new Storage(); Thread p1=new Thread(new Producers(storage),"生产者线程1"); Thread c1=new Thread(new Comsumers(storage),"消费者线程1"); p1.start(); c1.start(); } } class Comsumers implements Runnable { private Storage s; public Comsumers(Storage s) { this.s = s; } @Override public void run() { while(true){ synchronized (s) { try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } if(s.inventory() == 0){ try { s.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"消费--->"+s.remove()); s.notifyAll(); } } } } class Producers implements Runnable { private Storage s; public Producers(Storage s) { this.s = s; } @Override public void run() { while(true){ synchronized (s) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if(s.inventory() > 0){ try { s.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"生产--->"+s.add()); s.notifyAll(); } } } } class Ticket { private int no; public Ticket(int no) { this.no = no; } public String toString() { return no+"号票"; } } class Storage { private ArrayList<Ticket> tickets; private int no=1; public Storage(){ tickets=new ArrayList<Ticket>(20); } public int inventory(){ return tickets.size(); } public Ticket add(){ Ticket t = new Ticket(this.no++); tickets.add(t); return t; } public Ticket remove(){ return tickets.remove(0); } }
线程生命周期
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消 对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
注意在就绪状态的线程都有资格抢占cpu资源
2. 就绪状态(Runnable):
-
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
-
调用线程的start()方法,此线程进入就绪状态。
-
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
-
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
-
锁池里的线程拿到对象锁后,进入就绪状态。
3. 运行状态(Running):
线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
调用当前线程的yield()方法,当前线程进入就绪状态。
4. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
-
等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
-
同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
-
其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
反射机制
概述
反射机制:可以操作字节码文件 作用:可以让程序更加灵活。
存放在java.lang.reflect.*包下;
获取Class的三种方式
-
Class c = Class.forName("完整类名");
-
Class c = 对象.getClass();
-
Class c = 数据类型.class,包括基础数据类型 Class c = String.class;
反射机制相关的重要的类有哪些?
java.lang.Class:代表整个字节码,代表一个类型,代表整个类。 java.lang.reflect.Method:代表字节码中的方法字节码。代表类中的方法。 java.lang.reflect.Constructor:代表字节码中的构造方法字节码。代表类中的构造方法 java.lang.reflect.Field:代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)
获取了Class之后,可以调用无参数构造方法来实例化对象
Class c = Class.forName("java.util.Date");c代表的就是日期Date类型
Object obj = c.newInstance();实例化一个Date日期类型的对象
注意:newInstance()底层调用的是该类型的无参数构造方法。如果没有这个无参数构造方法会出现"实例化"异常。
如果你只想让一个类的“静态代码块”执行的话,你可以怎么做? Class.forName("该类的类名"); 这样类就加载,类加载的时候,静态代码块执行!!!!
获取文件绝对路径
前提:文件在类路径下(即src下,src是类的根路径)
用处:通用方式,不会受到环境移植的影响
-
String path = Thread.currentThread().getContextClassLoader() .getResource("写相对路径,但是这个相对路径从src出发开始找").getPath();
-
String path = Thread.currentThread().getContextClassLoader() .getResource("abc").getPath(); //必须保证src下有abc文件。
-
String path = Thread.currentThread().getContextClassLoader() .getResource("a/db").getPath(); //必须保证src下有a目录,a目录下有db文件。
-
String path = Thread.currentThread().getContextClassLoader() .getResource("com/bjpowernode/test.properties").getPath(); //必须保证src下有com目录,com目录下有bjpowernode目录。 //bjpowernode目录下有test.properties文件。
直接以流的形式返回: InputStream in = Thread.currentThread().getContextClassLoader() .getResourceAsStream("com/bjpowernode/test.properties");
ResourceBundle快速绑定资源文件
ResourceBundle bundle = ResourceBundle.getBundle("com/bjpowernode/test");
//注意:创建时不用new,文件名不用加properties String value = bundle.getString(key);
要求:
-
这个文件必须在类路径下
-
这个文件必须是以.properties结尾。
注解
import java.lang.reflect.Method; public class MyAnnotationTest { public static void main(String[] args) throws Exception { Class c=Class.forName("MyAnnotationTest"); Method m=c.getDeclaredMethod("doSome"); //如果该方法前有注解 if(m.isAnnotationPresent(MyAnnotation.class)) { //获取注解 MyAnnotation myAnnotation=m.getAnnotation(MyAnnotation.class); System.out.println(myAnnotation.password()); System.out.println(myAnnotation.username()); } } @MyAnnotation(username="KXXindy",password="123") public void doSome(){ } } import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD}) public @interface MyAnnotation { String username(); String password(); }
反射机制的应用
步骤:
-
获取类
-
通过getDeclared...获取属性、方法、构造器
-
调用
Object类是全部类的父类,随便指,且都能向下转型 import java.lang.reflect.Field; import java.lang.reflect.Modifier; public static void main(String[]args)throws Exception{ Class aClass=Class.forName("A");//获取类 Object aObject=aClass.newInstance();//实例对象 //获取属性,设置属性 Field aField=aClass.getDeclaredField("a");//获取该类的属性 aField.set(aObject,1);//对实例对象的属性修改 System.out.println(aField.get(aObject)); //获取方法,调用方法 Method aMethod=aClass.getDeclaredMethod("f",int.class,int.class);//获取到该类的方法 Object reValue=aMethod.invoke(aObject,3,4);//调用该方法(传入实例对象) System.out.println(reValue); //获取构造方法,实例化对象 Constructor con= aClass.getDeclaredConstructor(int.class,String.class); Object aObject2=con.newInstance(3,"a"); System.out.println(aObject2); }
网络编程
计算机网络相关知识省略
InetAddress
表示ip地址
//1. 获取本机的InetAddress 对象 InetAddress localHost = InetAddress.getLocalHost(); System.out.println(localHost); //2. 根据指定主机名 获取 InetAddress对象 InetAddress host1 = InetAddress.getByName("DESKTOP-MOQJRSN"); System.out.println("host1=" + host1); //3. 根据域名返回 InetAddress对象, 比如 www.baidu.com 对应 InetAddress host2 = InetAddress.getByName("www.baidu.com"); System.out.println("host2=" + host2);//www.baidu.com / 110.242.68.4 //4. 通过 InetAddress 对象,获取对应的地址 String hostAddress = host2.getHostAddress(); System.out.println("host2 对应的ip = " + hostAddress); //5. 通过 InetAddress 对象,获取对应的主机名/或者的域名 String hostName = host2.getHostName(); System.out.println("host2对应的主机名/域名=" + hostName); // www.baidu.com
Socket(TCP)
-
该类实现客户端套接字(也称为“套接字”)。 套接字是两台机器之间通信的端点。 (可以看作是插座,可以插上数据线进行数据传输)
-
通信两端都要有Socket,是两台机器间通信的端点
-
网络通信其实就是Socket间的通信
-
Socket允许程序把网络连接当作一个流,数据在两个Socket间通过IO传输
应用案例一.发送消息
服务器代码
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class SocketTCP01Server { public static void main(String[] args) throws IOException { //思路 //1. 在本机 的9999端口监听, 等待连接 // 细节: 要求在本机没有其它服务在监听9999 // ServerSocket 可以通过 accept() 返回多个Socket[多个客户端连接服务器的并发] ServerSocket serverSocket = new ServerSocket(9999); System.out.println("服务端,在9999端口监听,等待连接.."); //2. 当没有客户端连接9999端口时,程序会阻塞, 等待连接,如果有客户端连接,则会返回Socket对象,程 序继续 Socket socket = serverSocket.accept();//accept()方法返回与端口9999建立连接的服务器端 socket对象 System.out.println("服务端 socket =" + socket.getClass()); //3. 通过socket.getInputStream() 读取客户端写入到数据通道的数据 InputStream inputStream = socket.getInputStream();//从通信通道中读取数据 //4. IO读取 byte[] buf = new byte[1024]; int readLen = 0; while ((readLen = inputStream.read(buf)) != -1) { System.out.println(new String(buf, 0, readLen));//根据读取到的实际长度,显示内容. } //5. 获取socket相关联的输出流 OutputStream outputStream = socket.getOutputStream(); outputStream.write("hello, client".getBytes()); // 设置结束标记 socket.shutdownOutput();//结束输入到通信通道 //6.关闭流和socket inputStream.close();//关闭流 socket.close();//关闭当前与端口9999建立连接的通信通道 serverSocket.close();//关闭端口9999 } } ===================字符流操作======================= { BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));//获取字符输出流 bw.write("hello, server"); bw.newLine();//使用字符流时可以用newLine插入一个换行符,表示写入的内容结束(结束标志) bw.flush();//手动刷新,否则数据不会写入数据通道 BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));//获取字符输入流 String S=br.readLine();//字符读取时要使用readLine才能读到结束标志 System.out.println(S); bw.close(); br.close(); }
客户端代码
import java.net.InetAddress; import java.net.Socket; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; // 客户端,发送 "hello, server" 给服务端 public class SocketTcpClient01 { public static void main(String[] args) throws IOException { //思路 //1. 连接服务端 (ip , 端口) //解读: 连接本机的 9999端口, 如果连接成功,返回Socket对象 Socket socket = new Socket(InetAddress.getLocalHost(), 9999); System.out.println("客户端 socket返回=" + socket.getClass()); //2. 连接上后,生成Socket, 通过socket.getOutputStream()得到和socket对象关联的输出流对象 OutputStream outputStream = socket.getOutputStream();//获得建立通信通道的输出流 //3. 通过输出流,写入数据到数据通道 outputStream.write("hello, server".getBytes());//向建立的通信通道中写入数据 //设置结束标记 socket.shutdownOutput();//输出已结束 //4. 获取和socket关联的输入流. 读取数据(字节),并显示 InputStream inputStream = socket.getInputStream(); byte[] buf = new byte[1024]; int readLen = 0; while ((readLen = inputStream.read(buf)) != -1) { System.out.println(new String(buf, 0, readLen)); } //5. 关闭流对象和socket, 必须关闭 outputStream.close(); socket.close(); System.out.println("客户端退出....."); } }
应用案例2.传输文件
工具类
package com.KXXindy.Upload; import java.io.*; public class StreamUtils { //从字节输入流读入数据并存到bytes数组中 public static byte[] streamToByteArray(InputStream is)throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream();//创建输出流对象 byte[] bytes = new byte[1024];//创建字节数组bytes int len; while((len = is.read(bytes))!=-1){ //从字节输入流中读取内容到bytes数组 bos.write(bytes, 0, len);//将bytes数组的数据写入bos的内存缓冲区 } byte[] array = bos.toByteArray();//将bos转成字节数组 return array; } //直接从字节输入流读入数据并转为字符串(前提是数据确实为字符) public static String streamToString(InputStream is)throws Exception{ BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder stringBuilder=new StringBuilder(); String line; while((line = reader.readLine())!=null){ stringBuilder.append(line+"\r\n"); } return stringBuilder.toString(); } }
服务端代码
package com.KXXindy.Upload; import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class TCPFileUploadServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9999); System.out.println("服务器正在监听..."); Socket socket = serverSocket.accept(); //从socket信道读取数据 BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); byte[] bytes = StreamUtils.streamToByteArray(bis); //将数据写入服务器端磁盘 String filePath="src\\学习.jpg"; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath)); bos.write(bytes); System.out.println("收到文件"); //发送已收到消息 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bw.write("已收到文件"); bw.flush(); socket.shutdownOutput(); //关闭流 bw.close(); bos.close(); bis.close(); socket.close(); serverSocket.close(); } }
客户端代码
package com.KXXindy.Upload; import java.io.*; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; public class TCPFileUploadClient { public static void main(String[] args) throws Exception { Socket socket = new Socket(InetAddress.getLocalHost(),9999); String filePath ="src\\res\\学习.jpg" ;//一定是绑定到指定文件 //从指定文件读取数据,转为byte数组 BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));//转成buffered快一点 byte[] bytes = StreamUtils.streamToByteArray(bis); //通过socket信道输送数据 BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream()); bos.write(bytes); socket.shutdownOutput(); //接受消息 InputStream in = socket.getInputStream(); System.out.println(StreamUtils.streamToString(in)); //关闭流 in.close(); bis.close(); bos.close(); socket.close(); } }
netstat
-
netstat-an可以查看当前之际网络情况,包括端口监听情况和网络连接情况
-
netstat-an|more可以分页显示
说明:
-
Listening表示某个端口正在监听
-
如果有一个外部程序(客户端)连接到该端口,就会显示一条连接信息
TCP网络通讯的秘密
当客户端连接到服务端后,实际上客户端也是通过一个端口和服务端进行通讯的,这个端口是TCP/IP来分配的,是不确定的,是随机的(建立连接时,客户端的端口是随机分配的,而服务器端始终是固定。)
UDP
public class UDPReceiverA { public static void main(String[] args) throws IOException { //1. 创建一个 DatagramSocket 对象,准备在9999接收数据 DatagramSocket socket = new DatagramSocket(9999); //2. 构建一个 DatagramPacket 对象,准备接收数据 // 在前面讲解UDP 协议时,老师说过一个数据包最大 64k byte[] buf = new byte[1024]; DatagramPacket packet = new DatagramPacket(buf, buf.length);//接收数据也要先用封装打包的DatagramPacket对象接 //3. 调用 接收方法, 将通过网络传输的 DatagramPacket 对象 // 填充到 packet对象 //老师提示: 当有数据包发送到 本机的9999端口时,就会接收到数据 // 如果没有数据包发送到 本机的9999端口, 就会阻塞等待. System.out.println("接收端A 等待接收数据.."); socket.receive(packet);//等待数据(阻塞) //4. 可以把packet 进行拆包,取出数据,并显示. int length = packet.getLength();//实际接收到的数据字节长度 byte[] data = packet.getData();//接收到数据,拆包 String s = new String(data, 0, length);//toString,然后打印 System.out.println(s); //==============回复信息给B端================ //将需要发送的数据,封装到 DatagramPacket对象 data = "好的, 明天见".getBytes(); //说明: 封装的 DatagramPacket对象 data 内容字节数组 , data.length , 主机(IP) , 端口 packet = new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 9998);//向9998发送数据 socket.send(packet);//发送 //5. 关闭资源 socket.close(); System.out.println("A端退出..."); } }
public class UDPSenderB { public static void main(String[] args) throws IOException { //1.创建 DatagramSocket 对象,准备在9998端口 接收数据 //注意在UDP连接中,如果在同一台主机上,各个服务端占据属于自己的端口,进行接收发送数据的请求 DatagramSocket socket = new DatagramSocket(9998); //2. 将需要发送的数据,封装到 DatagramPacket对象 byte[] data = "hello 明天吃火锅~".getBytes(); // //说明: 封装的 DatagramPacket对象 data 内容字节数组 , data.length , 主机(IP) , 端口 DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("192.168.12.1"), 9999); socket.send(packet); //3.====接收从A端回复的信息================= //(1) 构建一个 DatagramPacket 对象,准备接收数据 byte[] buf = new byte[1024]; packet = new DatagramPacket(buf, buf.length);//packet已经重新引用了 //(2) 调用 接收方法, 将通过网络传输的 DatagramPacket 对象 // 填充到 packet对象 //老师提示: 当有数据包发送到 本机的9998端口时,就会接收到数据 // 如果没有数据包发送到 本机的9998端口, 就会阻塞等待. socket.receive(packet);//等待数据发到自己这里9998端口 //(3) 可以把packet 进行拆包,取出数据,并显示. int length = packet.getLength();//实际接收到的数据字节长度 data = packet.getData();//接收到数据 String s = new String(data, 0, length); System.out.println(s); //关闭资源 socket.close(); System.out.println("B端退出"); } }
Java机制
-
公共类与类文件名必须保持一致,并且一个java文件中只能有一个public关键字声明的类。公共类对所有类都可见。如果一个类没有修饰符(默认,也称为package-private),它只在自己的包(文件夹)中可见。
-
一个程序可以有多个类,每个类都能有一个主方法(入口)
-
println(括号内可直接调用方法)
-
Java中所有变量传递都是值传递
-
垃圾回收机制GC主要针对堆内存,不会回收常量
-
执行顺序:静态代码块,静态变量在类加载时期执行
实例代码块在构造函数执行前执行
-
OCP原则:开闭原则,“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中“闭”,是指对于原有代码的修改是封闭的,即修改原有的代码对外部的使用是透明的。
-
System.exit(0)退出当前正在运行的JVM(Java虚拟机),即退出当前进程
包和import
包其实就是目录,特别是项目比较大,java 文件特别多的情况下,我们应该分目录管理,不同功能的类分别放在不同的包下,在 java中称为分包管理
-
包最好采用小写字母
-
包的命名应该有规则,不能重复,一般采用公司网站逆序,
如:com.bjpowernode.项目名称.模块名称
com.bjpowernode.exam
-
package 必须放到 所有语句的第一行,注释除外
-
class 文件必须放到和包一样的目录里才可以,这就是采用包来管理类,也就是采用目录来管理类
-
如果采用了包,在执行该类时必须加入完整的包名称,正确的执行方式为 java com.bjpowernode.exam.PackageTest01
编译方式:javac -d . xxx.java 带包编译 生成的包放在当前目录下
import:采用 import 引入需要使用的类,可以采用 * 通配符引入包下的所有类,在package之下,class之上,如果都在同一个包下就不需要 import 引入了
java程序的加载和执行
正在上传…重新上传取消正在上传…重新上传取消正在上传…重新上传取消
Java的可移植性:JVM虚拟机
java程序在不同系统编译阶段形成的类文件(字节码)是相同的,然后进行类装载,再通过不同版本的jvm解释器进行解释生成不同的机器码来适配系统
对xxx.java文件的编译执行
编译:javac + 源文件路径(结尾是源文件)
路径可以是相对路径(相对于现在的位置)或绝对路径(相对于硬盘根目录的)
执行类文件:类文件所在的路径下 java+类名
常见dos命令:
-
dir 命令:查看当前目录下所有的子文件或子目录。
-
cd 命令:切换路径,使用方法是:cd 目录路径,需要注意的是路径包括相对路径和绝对路径,对于windows 来说从硬盘的根路径下开始的路径都是绝对路径,例如:C:\Program Files、C:\Program Files\Java 等,所有的相对路径都是从当前所在目录作为起点开始查找的路径。另外 cd ..切换到上级目录,cd \切换到根目录。
-
切换盘符:直接输入 c:,或者 d:,然后回车即可。切换盘符不需要 cd 命令。
-
del 命令:删除文件或目录,例如:del *.class,删除当前目录下所有的.class 文件
-
ipconfig 命令:查看 IP 地址等信息,查看更详细的信息使用 ipconfig /all。
-
ping 命令:查看两台计算机是否可以正常通信,例如:ping 192.168.1.100,正常情况下发送数据包和接收数据包数量相同并且没有延迟的时候表示通信正常,ping 192.168.1.100 -t 表示一直发送数据包和接收数据包,ping www.baidu.com 可以查看电脑是否可以正常上网。
-
mkdir 命令:创建目录,例如:mkdir abc 表示在“当前目录”下新建 abc 目录。
-
cls 命令:清屏。
-
exit 命令:退出 DOS 命令窗口