本文章是针对JavaSE进行知识总结,比较偏概念性的;需要相关md文件可以关注作者,由于图片格式问题,文中图片全部删除,纯素文章。最全知识总结~~~个人一手
JavaSE
流程控制–分支结构
if
switch
-
switch小括号中的数据类型只能是byte、short、char、int;1.5之后可以使用枚举类型;1.7之后使用String类型
-
case后只能跟常量不能跟变量;
-
case语句后面要跟上一个break语句,否则就无法结束switch语句,并且发生case穿透:
case穿透的利用: 多个case情况如果是相同的执行逻辑,可以在最后一个case语句后再使用break,在switch语句中,如果case的后面不写break,也就不会再判断下一个case的值,直接向后寻找一切可执行语句运行,直到遇到break,或者整体switch结束。
-
default语句: 可以写也可以不写.如果不写,就有可能一个语句都执行不到,建议把default语句加上.
-
default语句可以写在任意位置,无论写在什么位置,都是最后去匹配,建议把default语句写在最后(了解).
分支结构–循环结构
三种循环语句的区别:
1.dowhile语句和其他两种语句的区别:
1)for循环和while循环先判断条件是否成立,然后决定是否执行循环体(先判断后执行)
do...while循环先执行一次循环体,然后判断条件是否成立,是否继续执行循环体(先执行后判断)
2)dowhile语句至少可以执行一次,另外两种有可能一次都执行不了
2.while语句和for的区别:
1) 代码层面: while语句声明的初始化变量,在while结束之后,还能继续使用;
for中声明的初始化变量,在for结束之后,就无法使用了.
2)设计层面: 循环次数确定的话,建议使用for;循环次数不确定建议使用while
死循环
1)在格式上使用了死循环,但是在循环体中,可能出现了需要循环结束的情况,准备了一些跳出、终止循环的跳转语句.
2)在服务器的设计中,希望服务器永远给我们服务下去,所以需要使用死循环.
死循环之后的代码会报错, 因为代码永远不会执行到
嵌套循环
contiune
break
方法
1)提高了代码的复用性(反复使用)
2)提高了代码的封装性,大括号中的内容,其他调用者看不到也无法直接访问
3)简化了软件设计的思维难度
方法定义
1) 修饰符: 使用关键字, 表述方法的使用范围,包括特点, 目前与main保持一致. public static
为了在main方法中直接调用
2) 返回值类型: 方法执行完毕, 是否需要将直接结果进行返回
a: 如果需要返回, 将目标的数据类型作为方法的返回值类型(数据类型任意)
b: 如果不需要返回, 使用关键字 void表达返回值为空
3) 方法名: 符合小驼峰规范, 从第二个英文单词开始, 首字母大写
getSum() print()
4) 参数列表: 方法执行过程中, 是否需要外界提供数据资源
a: 需要, 将数据类型和个数罗列出来: 数据类型 变量名, 数据类型 变量名…
b: 需求, 小括号可以是空的
5) 方法体: 实现代码功能的逻辑, 逻辑任意
6) return语句: return是关键字, 表示返回, 结束方法
a: 如果方法有返回值类型, 必须return , 且return的结果与返回值类型一致
b: 如果方法没有返回值类型, 可以不写return, 加入一定想写, return;
注意: 每一个方法都表示独立的功能, 因此方法定义时, 不能嵌套, 必须独立
注意事项
1)方法不能嵌套定义,每一个方法都是独立的
2)方法的先后没有区别,都是平级关系
3)方法可以嵌套调用,甚至可以自己调用自己
方法的调用
直接调用、赋值调用、打印输出
-
方法没有返回值结果: 只能直接调用, 方法名(实际参数)
-
方法有返回值结果: 可以直接调用, 更建议赋值调用(最推荐), 将方法的返回值结果给某一个变量进行赋值
, 将结果保留下来, 后续代码中继续使用
- 输出打印调用: 只能使用在方法有返回值结果, 将方法的调用设计在输出语句中,
方法调用完毕, 结果直接输出
方法的重载 Overload
1.与返回值类型无关,与参数列表有关:参数列表的不同:排列顺序、参数的数据类型、参数个数
2.在功能上类似的三个求和方法,每个方法都有自己的方法名,不便于记忆,如果让类似功能具有相同方法名, 通过参数列表不同区分多个方法,如此也可以实现,这就是方法重载.
数组 存储相同类型数据的容器.
1)存储数据长度固定的容器, 当定义一个数组时, 必须指定数组的长度(可以存储的数据个数)
2)存储数据的数据类型要一致
3)new: 关键字,创建数组使用的关键字,用于在堆内存中给数组开辟存储数据的空间
数组动态初始化–》只指定数组长度不指定数组元素值.
数组静态初始化: 在创建数组时,直接将元素确定的定义方式;
数组的内存理解
内存是计算机中的重要原件,临时存储区域,作用是运行程序.;
Java虚拟机要运行程序,对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式.类比一处房子,房子中有厨房,专门进行做饭;有卧室,专门进行休息;有书房,专门进行学习和工作…因此内存中的空间就像房子要分门别类进行空间的管理使用.
数组在内存中的存储
数组引用的输出的结果是地址,是数组在内存中的地址.new出来的内容,都是在堆内存中存储的,而方法中的变量arr保存的是数组的地址.
引用数据类型每次new,都在内存中开辟新的空间地址.
多个引用指向同一个数组空间:任意一个引用修改了数组中的内容,其他引用访问到的就是修改后的数据.
数组的异常 空指针、数组索引越界
表示在Java代码编译和运行过程中,出现了不正确的,不正常的,不符合实际场景的情况,统称为异常
空指针异常(NullPointerException):当引用数据类型值设置为null(前提),证明这个变量引用没有指向任何堆内存地址,但仍然想通过这个变量引用访问堆内存中数据,就会报出空指针异常
null,即为空,用于表示在栈内存的引用中,不记录任何堆内存的地址
数组的操作 防止空指针 越界
- 遍历数组元素 循环0到lenght-1、索引
- 数组获最大值 擂台思想
- 数组元素交换 引入中间临时变量
- 数组的反转 (数组中对称位置的元素进行交换) 定义头和尾两个索引变量、同时向中间移动、交换变量指定位置元素
- 冒泡排序:将数组中的元素进行升序排序(从小到大)–》比较两个相邻的元素,将值大的元素交换至右端
面向对象与封装构造
对象内存图
- 要使用某一个类时, 这个类对应的.class字节码文件需要进入到方法区中(.class文件拥有.java文件中的一切功能), 有一个内存地址。
- 当创建出一个对象时, 需要在堆内存中进行空间开辟, 这个对象里面记录对应的.class文件的地址, 同时JVM会将类中的成员变量加载到对象中, 并且为其进行默认的赋初值动作。
- 如果对象调用了某方法, 那么这个方法在栈内存中就会记录这个对象的地址, 有后续数据的使用, 到该对象地址中进行获取。
匿名对象的使用 在内存中存在时间短, 相对节省内存
- 如果某个对象在创建之后,其方法只调用一次,那么就可以使用匿名对象来调用。这种写法会节省一些内存空间。
- 可以作为某个方法的实际参数。在被调用的方法中,这个对象是有引用的对象,不是匿名对象。
- 可以作为某个方法的返回值。这种调用形式,在被调用方法中,这个对象是匿名对象,但是在调用者,这个对象可能不是匿名对象。
基本类型和引用类型作为方法参数传递
成员变量和局部变量的区别
定义位置不同 内存位置不同(堆空间、栈空间) 生命周期不同(对象创建 方法执行)
封装 客观里的成员变量
- 将属性隐藏起来,使用时需要调用对外提供公共的访问方式
- 隐藏了事物的实现细节 提高了代码的复用性、代码的安全性
- private 修饰成员变量、成员方法、构造方法、内部类
- 只能在本类中使用。其他所有外类不能直接使用private修饰的成员,只能通过修饰所存在类中的getter/setter方法;对属性封装后,对外提供的统一访问属性的访问方式。
- 只能在本类中使用,其他所有外类不能直接使用private修饰的成员
getter&setter是对属性封装后对外提供的统一访问属性的访问方式
封装优化 把封装后对相关操作的修改操作称之为封装的优化
- 变量的就近访问原则 例:局部变量和成员变量同名时
- this关键字 所在类的当前对象的引用(地址值),即对象自己的引用。
构造方法
当一个对象被创建时,构造方法用来初始化该对象,给对象的成员变量赋初始值。构造方法有自己独有的方法格式。 有参构造、无参构造
- 方法名必须和类名一致 通常使用public
- 没有返回值类型,连void都不写
- 默认被jvm调用使用,构造方法调用时机:在创建对象时, JVM虚拟机主动调用,并且创建一次对象, 构造方法只调用一次, 构造方法无法手动通过对象名调用
注意事项:
- 如果不提供构造方法,系统会给出无参数构造方法。
- 如果提供了构造方法,系统将不再提供无参数构造方法。
- 构造方法是可以重载的,既可以定义参数,也可以不定义参数。
- 如果代码中定义了空参数构造, 也同时定义了有参数构造, 那么创建对象时, 可以从多个构造中选择其中任意一个调用即可。
JavaBean
Java语言编写类的一种标准规范。符合JavaBean 的类,要求
- 类必须是具体的和公共的
- 所有属性使用private修饰
- 提供用来操作成员变量的set 和get 方法
- 并且具有无参数的构造方法(建议有)
静态与继承
静态 static
主要用来修饰**java的变量 **和 方法的关键字
场景对比:
a : 没有静态
如果某个类型的所有对象,都具有一个相同的属性值,那么这个属性值就没有必要在所有对象中,都存储一份。还有坏处:浪费内存空间;维护难度大,一旦需要修改,就得修改所有的对象
b: 有静态
如果某个类型的所有对象,都具有一个相同的属性值,那么就在这个属性的定义上,加一个static静态关键字。让该变量存储在方法区字节码的静态区中,避免了所有对象都存储相同数据的问题,节省了内存空间,将来维护容易(只需要修改一次)
静态成员具有共享性,可以被所有的对象共享使用;静态成员优先于一切对象在内存中,静态属于类不属于任何对象,静态跟着类.class文件存储在方法区中。
当一个类型的所有对象都拥有某一个相同的属性值, 那么这个属性可以使用static修饰
静态变量的特点:
- 静态变量属于类, 不属于对象
- 加载时机:
随着类的加载而加载
静态变量随着类的加载进方法区,就直接在静态区给开辟了存储静态变量的内存空间
3. 静态变量优先于对象而存在
4. 静态变量被所有该类对象所共享
5. 调用方式:
类名调用(最推荐) 或者 创建对象调用(不推荐)
静态访问的注意事项(方法)
1、静态方法:在方法声明上,加上了static关键字的方法,就是静态方法
2、静态方法不能访问非静态的变量
原因:
静态方法可以在没有创建对象的时候调用,而非静态的变量只有在对象创建之后才存在。如果静态方法可以访问非静态的变量,那么就相当于在对象创建之前,就访问了对象创建之后的数据。明显不合理。
3、静态方法不能访问非静态的方法
原因:
静态方法可以在没有创建对象的时候调用;非静态的方法可以访问非静态的变量。如果静态方法可以访问非静态的方法,就相当于静态方法间接的访问了非静态的变量,和第2点矛盾。
4、静态方法中不能存在this关键字 //super
原因:
this关键字表示本类当前对象。静态方法可以在对象创建之前调用。如果静态方法可以访问this关键 字,相当于在创建对象之前,就使用了对象本身,矛盾
静态成员变量 和 非静态成员变量的区别
- 概念上,所属不同:
非静态变量属于对象
静态变量属于类,类变量
- 内存空间不同,存储位置不同
非静态变量属于对象,所以存储在堆内存中
静态变量属于类,存储在方法区的静态区中
- 内存时间不同,生命周期不同
非静态变量属于对象,所以生命周期和对象相同,随着对象的创建而存在,随着对象的消失而消失
静态变量属于类,所以生命周期和类相同,随着类的加载而存在,随着类的消失(内存管理)而消失
- 访问方式不同
非静态变量只能使用对象名访问
静态变量既可以使用对象访问,也可以通过类名访问:
类名.静态变量名 或者 对象名.静态变量
类名.静态方法名()
继承 属于关系
类与类产生子父类的关系.可以使得子类具有父类的属性和方法,还可以在子类中重新定义,以及追加属性和方法一种体现方式。 extends表示扩展、增加、继承
发生场景: 同类别的多个事物共性(成员变量、方法)向上抽取到父类中实现,剩下的类作为这个父类的子类,子类可以从父类中继承到共性内容,在子类中,再实现其特有的功能。
继承的好处:
- 提高了代码的复用性(多个类相同的成员可以放到同一个类中)共性内容
- 提高了代码的维护性(如果方法的代码需要修改,修改一次即可)
- 为多台提供了前提
- 继承中弊端:
继承让类与类之间产生了关系,类的耦合性增强了,当父类发生变化时子类实现也不得不跟着变化,削弱了子类的独立性
- 私有的成员不能被继承
父类中有一些私有成员,不能在子类中直接使用
其实在子类对象中,仍然包含了父类中定义的私有成员变量
只不过在子类中,不能直接访问父类中定义的私有成员变量
-
父类中的构造方法,不能继承
原因:
父类的构造方法需要和父类的类名一致、子类的构造方法需要和子类类名一致,父 类和子类的类名不一样。因此无法继承,名称有冲突。 父类的构造方法用于给父类的成员变量赋值,子类的构造方法用于给子类的成员变 量赋值,子类的成员变量较多,使用父类的构造方法无法将子类中所有的成员变量都进 行赋值,因此不继承父类的构造方法
继承的特点
- java支持单继承,不支持多继承,支持多层继承
单继承:一个子类只能继承一个父类
不能多继承:一个子类不能同时继承多个父类
可以多层继承:A类可以继承B类,B类可以继承C类,A类中拥有B、C类中的所有属性和方法。说明:越是顶层的类,定义的功能越是共性功能,功能和属性就越少;越是底层的类,定义的特有功能和属性就越多,就更加强大。学习一个体系的时候,先学顶层的类,共性功能学习完,学习底层特有的方法即可;使用一个类创建对象的时候,选择底层的类型,功能更多更强大。
如果支持多继承,那么可能一个子类继承了两个父类,两个父类中有相同的方法声明,却拥有不同的方法实现。子类继承之后,就不知道该走哪个父类的方法实现了(安全隐患)。
继承中成员变量的关系
- 父类中定义了成员变量,子类中没有定义,那么子类可以直接使用父类中非私有成员
- 父类中没有定义成员变量,子类中定义了,子类可以自己调用自己的变量,父类不能调用子类特有变量
总结: 子类可以使用自己和父类的成员变量,父类只能使用自己的成员变量
变量的访问就近原则
- 方法内部,自己定义了变量,使用方法内部变量
- 方法内部没有定义,找当前类中成员变量
- 类中成员变量也没有定义,找父类中的成员变量
- 父类中没有定义变量,再继续找父类的父类,直到找到最后(Object类,所有类的直接或者间接的父类,Object是一个最顶层的类,也没有找到,才报错
- 子父类中成员变量重复定义, 想调用父类成员变量, 可以使用关键字 super【super.父类成员变量名; 】
继承中成员方法的关系
- 父类中私有方法不能被子类继承使用
- 子类中定义的和父类中继承到的方法不同, 子类可以调用自己特有方法功能, 可以调用从父类继承来的功能
- 当子类需要父类的功能,而功能主体子类有自己特有内容时,可以重写父类中的方法,这样,即沿袭了父类的功能,又定义了子类特有的内容, 是对父类方法的功能进行了拓展和维护
- 重写Override注意事项:
- 重写需要与父类原方法, 返回值类型, 方法名 ,参数列表相同
- 私有方法不能被重写
- 子类方法访问权限不能更低(public > 默认 > 私有)
- 在子类重写方法上, 通常使用注解 @Override, 用来检测当前的方法, 是否是重写的方法,起到【校验】的作用
@Override:提高代码可读性; 对方法是否是重写, 注解有验证作用
什么是Overload(重载)? 什么是Override(重写,覆盖)?
重载: 定义在同一个类中, 方法名相同,参数列表不同(参数类型, 参数个数, 不同类型参数排列顺序),与方法返回值无关,让类似功能拥有相同方法名, 简化记忆的难度
重写: 在子父类继承关系中, 子类将父类中的方法进行重写, 进一步功能加工
权限修饰符: 大于等于父类; 方法上添加Override注解
super关键字
建立在继承关系中用来使用父类资源的关键字;
-
使用场景: 子父类中的成员变量或方法重名时, 可以使用super关键字表示父类成员调用
super只能访问父类中定义的成员变量
语法: super.成员变量名; super只能访问父类中定义的成员方法
语法: super.成员方法名(实际参数);
this和super
this表示本类当前对象的引用;this是对象中的一个变量,一个引用指向的是该对象本身,由于是子类对象的引用。所以既可以访问子类中特有的数据,也可以访问父类中定义的数据。
super表示本类当前对象父类的引用;super是本类当前的父类引用。表示当前对象中,父类部分的数据,这个关键字只能访问父类中的数据,不能访问子类中的数据,父类部分的数据,在子类对象中处于靠前表示和this 相同的地址值,但是 this的访问权限不同。
继承中构造方法的关系
- 父类中构造无法被子类继承, 但是子类构造方法中可以调用父类构造; 在子父类继承关系中, 父类优先于子类进入到内存, 父类中数据优先于子类进行初始化
- 如果子类构造方法中, 没有手动调用任何构造(本类, 父类),系统会默认在子类构造方法第一行调用super(); 目的是为了让父类中成员优先于子类进入内存中赋值
- 如果子类构造方法中, 手动调用本类或者父类构造, 那么系统不会再默认调用任何构造, 一律以手动调用构造为准
- super() : 父类空参构造调用. 必须写在构造方法第一位置上, 直接保证父类构造优先于子类进入内存运行
super使用场景,都是在子类中
1) super.成员变量: 调用父类成员变量
2) super.方法(实际参数): 调用父类方法
3) super(构造参数): 子类调用父类构造, 让父类先进入内存
this和super使用总结
-
含义:
this关键字表示本类当前对象的引用
哪个对象在调用this所在的方法,this就表示哪个对象
super关键字表示本类当前对象的父类引用
哪个对象在调用super所在的方法,super就表示哪个对象中的父类部分的数据
-
super和this都可以访问成员变量
super只能访问父类中定义的成员变量
super.成员变量名
this既可以访问子类中定义的成员变量,也可以访问父类中定义的成员变量
this.成员变量名
-
super和this都可以访问成员方法
super只能访问父类中定义的成员方法
super.成员方法名()
this不仅可以访问子类中定义的成员方法,也可以访问父类中定义的成员方法
this.成员方法名()
-
super和this都可以访问构造方法:this语句和super语句
this():访问本类的其他构造方法
super():访问父类的构造方法
代码块 4个
- 局部代码块 :定义在方法中,限定变量的生命周期;缩短局部变量的生命周期,及时的释放内存空间提升代码的运行效率;代码可以使用外部的所有资源,如果是在局部代码块中修改了局部代码块外声明的变量,局部代码块结束之后,并不会消除局部代码块对这个变量的修改。
- 构造代码块:定义在类中方法外[成员变量位置];创建对象的时候给对象的属性值显性赋值,提取本类构造方法中的共性内容实现;在创建的时候执行,由jvm默认调用;在构造方法之前,执行。
- 静态代码块:static{ ~~~ },类中方法外;给类的静态变量赋值,用来加载只需要加载一次的资源例子:数据库驱动;随着的加载而执行,静态代码只执行一次,执行的时机最早,遭遇所有对象的相关内容。 静态属于类,当类第一次进入到内存中,JVM虚拟机主动调用静态代码块一次
- 同步代码块(多线程):
静态上下文中不能使用非静态;非静态中可以使用静态;
内部类 定义在一个类中的另一个类 3
根据定义的位置不一样以及是否有名分为了不同的内部类,成员内部类,局部内部类,匿名内部类;
- 普通成员内部类 在类中方法外,跟成员变量是一个位置 ;内部类可以直接访问外部列,包括私有成员,外部类访问内部类的成员,必须创建内部类对象。
- 私有的成员内部类,成员内部类加上一个private;外部类以外,不能直接访问外部类中的私有成员内部类;在外部类中,定义一个访问私有成员内部类的公有方法,让外界可以调用公有方法,间接的访问外部类中的私有成员内部类。
- 静态的成员内部类,成员内部类前面加上一个static关键字;成员内部类是外部类的静态成员,所以可以通过外部类名.内部类名的方式直接访问,而不需要创建外部类的对象。静态内部类中的非静态成员,需要将所在的内部类对象创建出来之后,才能被调用。一个类是否需要创建对象,不取决于该类本身是否是静态,而取决于要访问 的成员是否是静态
- 局部内部类:局部内部类是定义在方法中的类;局部内部类,外界是无法直接使用,需要在方法内部创建对象并使用;该类可以直接访问外部类的成员,也可以访问方法内的局部变量;
- 匿名内部类
访问特点:内部类可以直接访问外部类的成员,包括私有成员;外部类要访问内部类的成员,必须要创建对象。
final
可以修饰类、方法、变量(成员变量、局部变量)
- 修饰类
(1) 表示一个最终类,表示不能有子类,【不能被其他类继承】
(2) 一旦一个类型不能被继承,那么其中所有的方法都不能被重写
(3) 不影响当前类的方法被调用
-
修饰方法:表示一个最终方法,【该方法不能被重写】
-
修饰变量:
(1) 表示一个最终变量,该【变量变成了常量】,就只能赋值一次
(2) 当前项目中,常用的常量一般都定义在一个类的成员位置,甚至专门定义一个常量类,常量命名规范: 所有单词全大写, 多个单词之间使用_进行分隔了,举例: SCHOOL_NAME PI
(3) 定义常量的好处:见名知意,容易理解;可维护性高
包
权限修饰符
private :私有的 限定范围:本类中使用
- 成员变量(最常见)
- 修饰方法
- 修饰内部类
- 修饰构造方法(单例模式,枚举类型,暂时没讲)
默认的:啥也不写 限定范围:本类和本包中使用
定义类,方法,变量, 没有添加任何的权限修饰, 那么默认修饰权限
protected:受保护的 限定范围:本类和本包中使用,不同包下的子类内部可以使用
修饰变量, 修饰方法
public :公共的 限定范围:没有范围
最常见修饰类(JavaBean), 修饰方法
多态与抽象接口
多态
对象的不同数据类型的体现,也就是子类对象充当父类类型对象;
多态发生的前提:继承、实现关系;有方法的重写(方法名、返回值类型、参数列表相同、具体的实现不同);父类引用指向子类的对象。 父类类型 变量名 = new 子类名(实参);
**多态中成员变量的访问原则:**编译看左,运行看左;
**多态中成员方法的访问特点:**编译看左【多态表达式对象调用方法时, 验证, 等号左边是否有该方法,】,运行看右【根据多态表达式, JVM进行方法调用时, 动态绑定等号右边的类型中的方法】;
向上向下转型
-
向上转型:
使用子类的引用指向子类的对象(正常情况)
多态中,使用父类的引用指向子类的对象(向上转型)
本质:缩小了对象本身的访问范围,减少了访问的权限(只能访问父类中定义的内容)
-
向下转型:
让指向子类对象的父类引用,【恢复】成子类的引用;
子类类型 引用名称 = (子类类型)父类类型的引用;
【恢复】子类类型原本就有的访问范围
注意:
向下转型的时候有可能转为其他子类的类型,编译不会报错但是运行时会发生类型转换异常
解决方案:转之前判断一下要转换的类型是不是多态对象之前的类型。使用一个关键字 instanceof 来判断向下转换类型是不是自己的(子类)类型 person instanceof Doctor对象是否可以转换成对应的类型
**好处:代码中已经有了多态向上转型表达式了, 所以不进行额外内存空间的占有情况下, 利用向下转型取出子类的特有。
多态的好处
- 提高了代码的扩展性(灵活性)
- 在方法的参数列表中,形式参数是父类类型的引用,将来调用方法的时候,父类类型的任意子类对象,都可以作为方法的实际参数。
- 不在方法的参数列表中,就在普通的方法体中,使用父类的类型指向子类的对象,也能提高代码的可扩展性。对象的来源非常广泛,不仅仅是new出来的,(还可能是通过反射获取的,通过文件读取的,还可能是网络传递的,在写代码的编译阶段,无法知道对象具体的子类类型的)需要使用父类类型的引用,操作不知道的子类类型的对象。
抽象类 定义抽象方法的类
java中只有方法声明没有方法实现并且被关键字abstract修饰的方法;提取相同功能不同结果的方法声明;抽象方法只定义在抽象类和接口中。
-
抽象类和抽象方法都需要使用abstract关键字修饰
抽象类:abstract class {}
抽象方法:public abstract void test();
-
抽象类和抽象方法的关系:
抽象方法所在的类必须是抽象类
抽象类中未必一定都定义抽象方法,抽象类中未必存在抽象方法
-
抽象类的实例化
抽象类不能直接实例化
定义抽象类的子类,由子类创建对象,调用方法
注意 : 抽象类存在的意义就是为了有子类继承, 重写抽象方法;
-
抽象类子类的前途:
在子类中,将父类所有的抽象方法全部重写(实现),子类就成了一个普通类,就可以创建对象。
在子类中,没有将父类中所有的抽象方法全部实现,子类就还是一个抽象类,还需要使用abstract关键字修饰子类。
抽象类中的成员
普通类 + 抽象方法 = 抽象类
成员变量:可以定义比那里,也可以定义常量,但是不能被抽象;
构造方法: 虽然抽象类无法创建对象,但是抽象类一定有子类,子类会访问父类的抽象方法。
是否有构造方法,不取决于是否可以创建对象,而是取决于是否可以定义成员变量。如果可以定义成员变量,那么就需要初始化成员变量,就是构造方法来完成。
成员方法:既可以是抽象方法(强制子类重写),也可以是非抽象方法;用于给子类继承提高代码的复用性。
接口 interface 用来描述多种不同规则的几何体
规则在java中就是抽象方法,接口就是存放了不同的抽象方法的几何体。
命名规则定义出来,这样的话 方法的调用 和 方法的实现就分离开了,可以提高开发效率,降低代码的耦合性。
使用关键字interface表示,接口和类同级别的, 接口的源文件也是.java文件,照样参与编译,编译后的文件依然是字节码文件。
属性: 接口中的成员变量实际是成员常量,默认被public static final修饰(使用接口名访问即可)。
方法: 在JDK1.8之前,只有抽象方法,默认被public abstract修饰。
- 抽象方法必须需要类来实现之后被类的对象调用使用;
- 没有构造方法,不能直接创建对象的;
是否有构造方法与能否创建对象实例化无关,与有是否有成员变量有关。
接口实现 implements
接口里的规则要想被使用,需要类来重写,使类拥有接口中的规则功能。如此类和接口 就发生关联关系,类去重写接口规则的过程就叫实现接口。
-
是一个抽象类,该类没有实现接口中的所有抽象方法
-
是一个普通类,该类实现了接口中的所有抽象方法
-
单实现:一个类实现了一个接口的实现方式,重写接口中所有抽象方法。
-
多实现:一个类同时实现多个不同接口的实现方式,不同的接口中相同方法声明的抽象方法只需要重写一次,重写几口中所有抽象方法
-
接口中的方法默认被public abstract修饰;
抽象类:定义事物本身具有的固有属性和行为
接口:定义事物通过学习、训练而扩展出来的行为
引用类型
API的使用方式
java的源代码编译之后,形成.class的字节码文件,包含了这个类中的所有内容,因此如果想在脱离源代码的前提下使用.class字节码文件,可以打包生成jar包(在Java中.class字节码文件的标准压缩包就是jar包)。
API(Application Programming Interface,应用程序接口)是一些预先定义的接口
(如函数、HTTP接口),或指软件系统不同组成部分衔接的约定.
Object类
-
Object是类层次结构的根类.每个类都使用Object作为超类.所有对象(包括数组)都实现
-
创建Object类的对象的方法.
-
子类访问,所有子类都会直接或者间接的访问到这个顶层父类的构造方法.
-
toString方法:返回当前对象的字符串表示.默认Object类的toString方法,由 getClass().getName() + ‘@’ + Integer.toHexString(hashCode())这几部分组成.。
-
重写的原则:返回该对象中的所有成员变量的值(对象的属性)
-
使用打印语句打印一个对象,其实打印的就是这个对象的toString()方法结果.
-
equals方法:指示其他某个对象是否与此对象"相等". ,.对于任何非空引用值x和y,当且仅当x和y引用同一个对象时,此方法才返回true(x == y 具有值true).也就是在Object类型中,比较的是两个引用是否指向了同一个对象.如果是,才返回true.相当于是在比较两个对象的地址值是否相同.。比较两个对象的内存地址,没有什么意义.因此在自定义的子类中,都要重写这个方法。重写原则:一般比较两个对象中的所有属性,是否全部相同
== 和 equals方法的区别:
1、比较内容的不同,==可以比较任意数据类型,既可以比较基本数据类型,也可以比较引用数据类型;而equals只能比较引用类型数据。
2、比较规则不同:==在比较基本类型的时候,比较的是数据的值,比较引用类型时,比较的的是地址值。
equals方法重写之前比较的是两个对象的地址值,在重写之后比较属性的值。
String
Java中只要使用双引号引用起来的任何数据它们都是String的一个对象,字符串面值属于常量,存储在方法区的常量池中,在创建之后就无法更改(是一个不可变的字符序列),但是它们可以被共享。字符串在效果上,可以看做是字符数组,但实质上是字节数组。
构造方法 6 个:空参、常量池中参数 (字符串)、字节数组、字节数组的一部分、字符数组、字符数组的一部分。
字符串的比较规则:第一种:public boolean equals(Object anObject):String类重写的方法,判断两个字符串的内容(字符序列)是否相等; 第二种:在比较基本类型的时候,比较的是数据的值,比较引用类型时,比较的是地址值是否相等。
字符串常用的方法:
Strign类的获取和查找功能 7个:
-
length() : 获取到一个字符串中的字符个数(字符串的长度) , 返回值类型int
-
charAt(int index): 将字符串中指定的index索引位置上对应的字符获取到,返回值类型char
-
subString(int beginIndex): 截取出字符串的一部分,从beginIndex索引开始, 到最后的全部字符序列取出来, 返回值结果,新的字符串String
-
subString(int beginIndex, int endIndex) : 从beginIndex到endIndex-1之间的字符序列截取成一个新的字符串, 返回值结果String
注意: JDK的方法中, 如果是对于两个索引位置进行操作, 通常包括开始索引, 不包括结束索引 -
indexOf(String str) : 获取参数字符串str在方法调用字符串中第一次出现的索引位置
返回值类型int, 如果没有找到参数str存在位置, 返回-1 -
indexOf(String str, int formIndex):返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索, 返回值类型int, 如果没有找到参数str存在位置, 返回-1
-
lastIndexOf(String str) : 获取参数字符串str在方法调用字符串中最后一次出现的索引位置
返回值类型int, 如果没有找到参数str存在位置, 返回-1
String类中的判断功能 4个:
- equalsIgnoreCase(String anotherString) : 将调用方法字符串与参数列表中的字符串进行内容比较,不考虑字母大小写。
- startsWith(String s) : 调用方法字符串是否以参数s字符串为开始(前缀)
- endsWith(String s): 调用方法字符串是否以参数s字符串为结束(后缀)
- isEmpty(): 验证调用方法的字符串是不是一个空字符串””, 如果是空字符串返回true, 否则返回false
String类的转换功能 5个: 字节 字符 大 、 小写; 任意转成字符串
- byte[] getBytes():将当前字符串,转成字节数组
- char[] toCharArray():将当前的字符串,转成字符数组
- toUpperCase():将当前的字符串,转成全大写形式
- toLowerCase():将当前的字符串,转成全小写形式
- static valueOf家族:可以将任意数据类型的数据,转换成字符串
String类其他功能 3个: 替换、 切割(regex)、 去空格、制表符
- String replace(String oldStr, String newStr):将调用者中的老串替换成新串,返回值为替换后的新字符串
- split(String regex): 将字符串按照参数字符串regex规则进行切割, 结果是一个String[]
- trim():去掉字符串左右两边的空格、制表符
StringBuilder
一个可变的字符序列,常用的方法是append和insert,就是在StringBuilder对象本身上,进行相关操作。
- StringBuilder底层和String类型一样,也是维护了一个字符数组,外界无法直接访问.
- String类型和StringBuilder的关系:都是用于描述字符串.但是前者内容是不可以改
变的,后者内容是可以改变的. 做字符串拼接时, 使用StringBuilder优化内存的使用.
StrignBuilder称为字符缓冲区,也可以称为字符生成器。
构造方法: 创建当前对象、将其他类型的数据,转换成当前类型
StringBuilder():创建一个生成器,初始容量为16个字符
StringBuilder(int capacity):创建一个生成器,初始容量为capacity大小
StringBuilder(String str):创建一个生成器,初始值就是str这些字符,初始大小是str+16
获取当前生成器的容器大小.capacity(); length()获取当前生成器中的时机字符个数。
常用功能:
添加功能:
- StringBuilder append(任意类型):可以将任意数据类型,转成字符,添加到生成器中
- insert(int index, 任意数据类型):可以将任意数据类型,添加到指定的位置
删除功能:
- deleteCharAt(int index) :删除指定索引的字符
- delete(int startIndex, int endIndex):删除指定范围的字符,被删除的部分包含头不包含尾
替换 和 反转功能
-
replace(int startIndex, int endIndex ,String str):
将字符串缓冲区中的从start开始到end-1这部分内容,替换成str
-
reverse():将原有字符序列进行反转
基本类型的包装类
- 基本数据类型有八种,都是非常简单的数据类型。
- 在这些类型中,只能表示简单的数据,不能包含一些操作数据的方法,没有办法存储描述这些数据的内容。需要准备一个引用数据类型,能将基本数据类型进行包装,提供一些操作基本类型的方法,定义一些描述数据的数据。
构造方法:
-
Integer(int i):将一个基本类型的int数,转换成Integer类型的对象
使用i给Integer对象中的成员变量赋值
-
Integer(String s):将一个字符串类型的数字,转换成Integer类型的对象
转换的作用
Integer类型的成员方法:
-
xxxValue():可以将Integer类型的对象,转成其他的基本数据类型:
例如:byteValue()、floatValue()
-
静态方法:
parseInt(String str):将str以十进制的方式解读为一个int数
自动装箱 和 拆箱
装箱:将基本数据类型,封装成包装类型的对象,就是装箱,使用构造方法即可
拆箱:从包装类型的对象中,将其包装的基本类型数据取出,就是拆箱,使用intValue方法即可
到了JDk1.5版本: 装箱和拆箱操作,不需要通过任何方法作为媒介转换, 可以自动完成
自动装箱:可以直接使用基本类型数据,给对应的引用数据类型包装类对象变量赋值
自动拆箱:可以直接使用包装类对象,给基本类型变量赋值或与基本类型直接运算
Math: E PI
E:自然对数的底数,2.718
PI:圆周率
常用方法:
abs(数字类型),返回该参数的绝对值
ceil(double d),返回d的向上取整
floor(double d),返回d的向下取整
max(int a, int b),返回a、b的较大值
min(int a, int ),返回a、b的较小值
pow(double a, double b),返回a的b次幂
random(),返回0.000~0.999的随机数
round(double d),返回d四舍五入的结果
System类 及其 常用方法
-
用于描述系统资源的类型,System是一个和系统相关的类,里面提供了一些系统常用的方法,例如调用垃圾回收机制,给出系统时间,关闭虚拟机等。该类不用创建对象,因为构造方法私有, 直接使用静态变量和静态方法即可。
-
常用字段(静态常量):
System.in:标准输入流,默认关联到键盘上
举例 : Scanner sc = new Scanner(System.in);
System.out:标准输出流,默认关联到控制台上
举例 : System.out.println(数据);
System.err:标准错误输出流,默认关联到控制台上,用于打印错误信息,使用该流打印的内容是红色
- 常用方法全部都是static修饰,currentTimeMillis():返回当前时间的毫秒值。1、可以通过某些手段,将数字转换成时间对象、指定格式的字符串;2、可以通过计算两次的差值,来获取某段代码运行的时间
- static void exit(int status): 退出JVM虚拟机,参数0表示正常终止。
时间类
Date表示特定的瞬间,精确到毫秒.是java中提供的表示时间日期数据的对象,但是这个类,其中大部分的方法已经过时,由Calendar和DateFormat类代替.
Date类构造方法:
- public Date(): 分配 Date 对象并初始化此对象,以表示分配它的时间(精确到毫秒)
- public Date(long date): 分配 Date 对象并初始化此对象,以表示自从标准基准时间(称为"历元(epoch)",即 1970 年 1 月 1 日 00:00:00 GMT)以来的指定毫秒数
Date构造方法:
- long getTime():
返回自1970 年1月1日 00:00:00 GMT 以来此 Date 对象表示的毫秒数
- void setTime(long time)
设置此Date对象,以表示 1970 年1月1日 00:00:00 GMT 以后 time 毫秒的对应时间点
SimpleDateFormat类及其常用方法
简单的日期格式化类,提供了日期的格式化的方法。
常用的构造方法:
- SimpleDateFormat(): 用默认的模式和默认语言环境的日期格式创建对象
- SimpleDateFormat(String pattern):用给定的模式和默认语言环境的日期格式创建对象,
常用的方法:
- final String format(Date date): 将一个 Date 格式化为日期/时间字符串
- Date parse(String source) throws ParseException: 从给定字符串解析文本,以生成一个日期。
Arrays工具类
- Arrays工具类中封装了对于数组进行操作相关功能, 来自于java.util包, 因为Arrays中的构造私有化,因此所有的方法都是静态修饰,类名.直接调用
- void sort(Object[] obj): 将参数数组按照升序进行排序(从小到大)
- String toString(Object[] obj): 将参数数组中的所有元素获取出来,拼接成字符串进行返回
- int binarySearch(Object[] obj, Object key): 从数组obj中查找key的位置。学名二分查找或者折半查找, 查找速度很快, 要求数组obj中的元素必须升序;key在数组最多出现一次;
异常
- Java中的每一种异常都封装成一个类, 当异常发生时创建出异常对象,对象中包含了异常情况的原因、类型、描述以及位置。
- 异常也是一种处理异常情况的机制,可以进行跳转、捕获以及结束程序。
异常的体系
- Throwable:可抛出的,是异常体系的顶层父类,其他的异常或者错误都是Throwable的子类类型,只有Throwable的体系类型,才可以使用异常的处理机制
- Error:错误,是Throwable的子类,用于描述那些无法捕获和处理的错误情况,属于非常严重的错误,StackOverflowError
- Exception:异常,是Throwable的子类,用于描述那些可以捕获和处理的例外情况,属于不太严重的错误,ArrayIndexOutOfBoundsException
- RuntimeException:运行时异常,是Exception的特殊的子类,在编译阶段不做检查的一个异常
- 编译时异常。
在jvm中默认处理异常的机制
- 在代码的某个位置,出现了和正常情况不同的情况,就将异常情况封装到一个异常对象中。
- 将异常对象抛给调用该方法的方法
- 某个方法接收到底层方法抛上来的异常,也没有办法自己处理,继续向上抛出,最终抛给主方法,主方法也没有办法处理,抛给调用自己的jvm虚拟机
- jvm虚拟机是我们手动调用的,只能将异常对象的所有信息,通过错误流打印出来,结束jvm虚拟机
- 总结,jvm默认处理的方式:【一层一层向上抛,jvm接收到之后结束自己】
手动处理异常的方式
- 有两大类处理异常的方式:
异常的声明:某个方法有编译时异常,编译就会无法通过,需要在异常所在的方法声明上,声明该方法可能出现的编译时异常
异常的处理:出现异常之后,可以通过某些格式来捕获和处理异常,可以让程序在出现异常之后,继续运行。可以定义自己处理异常的逻辑。
-
捕获处理异常的代码格式:
try…catch
try…catch…finally
try…finally(无法捕获处理异常)
运行时异常 和 编译时期异常的区别
- 编译时期异常:
a: Exception类和Exception除了RuntimeException以外的其他子类
b: 出现就必须显示的处理,否则程序无法编译通过,程序也就无法运行; 当代码中发生编译时期异常, 提示进行处理
- 运行时异常:
a: RuntimeException类和RuntimeException的子类
b: 出现无需显示处理,也可以像编译时一样显示的处理,无论是否处理程序都可以编译通过。
throw关键字 每一次只能抛出一个异常、使用在方法中
-
throw:抛出,用于抛出一个异常对象
-
throw关键字的使用场景 : 异常是一个对象,当程序运行到某种情况时,程序员认为这种情况和现实生活不符合,就把当前的对于情况的描述,封装到一个异常对象中,通过throw关键字将异常对象进行抛出。将异常抛给方法的调用者
-
throw关键字的语法结构:
throw new Exception或者Exception任意一个子类对象(“将异常发生详细信息描述清楚”);
-
作用:创建一个异常对象,使用throw关键字抛出,实现了程序的结束或者跳转。
-
说明:如果抛出的是编译时异常,那么这个异常必须使用异常处理的方式处理,才能编译成功;如果抛出的是运行时异常,在编译阶段就相当于没有异常,可以不处理这个异常。
throws关键字
-
throws:用于声明异常类型
-
在某个方法中,有一些编译时异常,没有给出处理的方案,没有捕获这个异常,没有处理这个异常,就说明这个方法是一个有问题的方法。为了让调用者在调用时,可以考虑到处理这个异常,所必须在当前方法的声明上,声明这个异常。
-
如果抛出的是一个运行时异常,那么就相当于没有抛出异常,这种异常也不需要在方法上声明;声明了一个运行时异常,也相当于没有做任何声明
-
如果抛出的是一个编译时异常,那么就必须进行声明或者捕获;如果声明了一个编译时异常,将来调用这个方法时,也相当于有一个声明的异常
throw和throws的比较
- throw是对异常对象的抛出,throws是对异常类型的声明
- throw是对异常对象实实在在的抛出,一旦使用了throw关键字,就一定有一个异常对象出现;throws是对可能出现的异常类型的声明,即使声明了一些异常类型,在这个方法中,也可以不出现任何异常。
- throw后面只能跟一个异常对象;throws可以跟很多个异常类型。
异常体系中的常用方法
-
发现在异常的继承体系中,所有的方法定义在了Throwable这个顶层父类中,子类中几乎没有什么特有方法
-
Throwable中的构造方法:
Throwable():创建一个没有任何参数的异常对象
Throwable(String message):创建一个带有指定消息的异常对象
-
常用成员方法:
getMessage():获取异常的详细信息
toString():获取异常对象的详细信息
printStackTrace():打印异常的调用栈轨迹(有关异常的方法调用路径), 对于异常信息追全面的输出方式, 本身返回值结果void, 但是在方法中已经有打印功能, 将异常进行输出,包括异常类型, 异常详细信息, 代码发生异常的行数
异常的注意事项
- 运行时异常抛出可以不处理,既不捕获也不声明抛出; 编译时期异常, 必须进行异常处理,throws声明异常, 或者try…catch语法结构捕获处理异常
- 如果父类抛出了多个编译时异常,子类覆盖(重写)父类的方法时,可以不抛出异常,或者抛出相同的异常,或者父类抛出异常的子类。
- 如果父类方法没有异常抛出,子类覆盖父类方法时,不能抛出编译时异常。
自定义异常
- jdk中提供了很多的异常类型,其中的绝大部分都没有自己特有的方法,都无法描述当前的异常情况,就需要我们自己定义异常类型,用在自己的项目的业务中。
- 自定义异常的步骤:
(1) 定义一个类,以Exception结尾,IllegleAgeException,表示这是一个非法年龄异常
(2) 让自己定义的这个类,继承一个Exception或者是RuntimeException
如果定义的是编译时异常,就使用Exception
如果定义的是运行时异常,就使用RuntimeException
(3) 构造方法不能被继承,需要手动添加
集合
集合 与 数组的区别:
量这个都是数据存储容器,可以存储多个数据。
不同点:数组的长度是不可变的,集合的长度是可变的;数组可以存基本数据类型 和 引用数据类型;
集合只能存引用数据类型,如果要存基本数据类型,需要存对应的包装类。
集合体系结构:
- Conllection接口 单列集合
- List:元素存取有序 ;元素有索引;存储的元素可以重复
- Set:元素存取无序;没有索引;不存储重复元素
- Map接口: 双列集合的顶层接口
顶层接口Collection
Collection是单列集合的顶层接口,定义的是所有单列集合中共有的功能. JDK不提供此接口的任何直接实现.它提供更具体的子接口(如Set和List)实现。并且不能直接创建对象,因此找一个实现类ArrayList创建对象。
- 多态: 多态的方式 – 父类引用指向子类对象 Collection c = new ArrayList();
- 具体的实现类创建 – 本类引用指向本类对象 ArrayList a = new ArrayList();
Collection常用的方法:
-
boolean add(Object e): 添加元素
-
boolean remove (Object o): 从集合中移除指定的元素
-
void clear(): 清空集合中的元素
-
boolean contains(Object o): 判断集合中是否存在指定的元素
-
boolean isEmpty(): 判断集合是否为空(集合存在,没有元素), 如果集合为空, 那么返回true, 如果集合不为空 false
-
int size(): 返回集合中元素的数量,返回集合的长度。
-
Object[] toArray(): 返回集合的对象的数组
-
利用方法进行集合的遍历
toArray–>集合转换成成数组; 默认转换成Object
遍历以上操作获取的数组,间接遍历集合。
单列集合的遍历
迭代器遍历:专门对于单列集合进行遍历的对象, 称为迭代器。
Iterator iterator(): 返回此集合中元素的迭代器,通过集合对象的iterator()方法得到;
Iterator中的常用方法:
- boolean hasNext(): 判断当前位置是否有元素可以被取出
- E next(): 获取当前位置的元素,并且将迭代器对象移向下一个索引位置
- void remove(): 删除迭代器对象当前指向的元素
// 1. 获取到coll集合的迭代器对象
// 接口是Iterator, 证明方法实际返回值Iterator接口的实现类对象
// 父接口 引用 = new 实现类对象();
Iterator it = coll.iterator();
// 2. 验证集合coll中是否还有下一个元素
while(it.hasNext()){
// 3. 获取出目前正在验证的数据
Object obj = it.next();
String s = (String)obj;
System.out.println(s);
}
增强for遍历:JDK5之后出现的,其内部原理是一个Iterator迭代器;实现Iterable接口的类才可以使用迭代器和增强for简化数组和Collection集合遍历。增强for(foreach)
for(数据类型 变量名 : 数组/单列集合){}
for(Object obj : c){System.out.println(obj); }
有序单列集合List
List集合是Collection接口的子接口,其下有两个常用实现类分别为 ArrayList 和 LinkedList;
有序、有索引、可以重复。
Lsit特有的方法:
- void add(int index,E element): 在此集合中的指定位置插入指定的元素
- E remove(int index): 删除指定索引处的元素,返回被删除的元素
- E set(int index,E element): 修改指定索引处的元素,返回被修改的元素
- E get(int index):返回指定索引处的元素
- 特有的遍历方式:可以通过集合的size方法获取list集合索引的范围,根据索引通过get方法可以获取指定索引的值。
for(int index = 0; index < list.size(); index++){
System.out.println(l.get(index));
}
并发修改异常【ConcurrentModificationException】的产生原因 和 解决办法
- 产生原因:在迭代器遍历过程中使用集合的引用进行元素的添加或删除;
当使用迭代器进行Collection(单列集合)迭代遍历时, 不允许同时对集合中的元素进行修改操作,
未来可能会发生迭代不准确的问题(并发修改异常典型原因: 迭代器遍历的同时, 使用集合对象修改
集合元素, 迭代器未来的遍历可能会大发生问题)
- 解决方法
(1) 通过for循环遍历集合,在遍历过程中可以进行添加和删除元素
for(int index = 0; index < list.size(); index++){
String s = (String)list.get(index);
if("hello".equals(s)){
// 原来 hello-->2
// 在0索引添加end, hello-->3
// "end",”ok”,”java”,”hello”,”world”
// "end", "end",”ok”,”java”,”hello”,”world”
// list.add(0,"end");
list.add("end");
}
(2) 使用迭代器中的remove()方法删除集合元素
(3) 使用List集合的特有迭代器ListIterator, 通过List中的方法listIterator()获取, 该迭代器允许迭代期间修改列表
– add(E e) 添加元素
– remove() 移除元素
ListIterator it = list.listIterator();
while(it.hasNext()){
String s = (String)it.next();
if("hello".equals(s)){
// 千万不要让集合对象向集合进行元素操作
// 必须使用ListIterator对集合中进行元素操作
it.add("end");
}
数据结构 可以带来更高的运行和存储效率
**栈:**stack,又称堆栈,它是运算受限的线性表,其限制是仅允许在标的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。
**特点: **先进后出,(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。
队列: queue,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。
**特点:**先进先出;队列的入口、出口各占一侧。
数据结构之数组和链表
数组:Array,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。查询
快、增删慢。
**链表:**linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。有单向链表和双向链表。其特点如下:
查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素
增删元素快: 增加元素 --> 只需要修改连接下个元素的地址即可。
删除元素:只需要修改连接下个元素的地址即可。
ArrayList 和 LinkedList对比分析:
ArrayList:
(1) 在创建ArrayList集合对象的时候,会维护一个长度为10的Object类型的数组.
(2) 当插入数据10个长度不够,这时候以1.5倍的形式进行扩容 元素赋值移动扩容
(3) 存储特点 : 查询快, 增删慢
LinkedList:集合数据存储的结构是链表结构。方便元素添加、删除的集合。
LinkedList是一个双向链表,特点 : 查询慢, 增删快, 链表结构不需要连续内存空间, 可以利用内存中零散空间.
首尾操作快。
LinkedList方法:
public void addFirst(E e):将指定元素插入此列表的开头。
public void addLast(E e):将指定元素添加到此列表的结尾。
public E getFirst():返回此列表的第一个元素。
public E getLast():返回此列表的最后一个元素。
public E removeFirst():移除并返回此列表的第一个元素。
public E removeLast():移除并返回此列表的最后一个元素。
泛型 提高数据安全性,将运行时的问题,提前暴露在编译时期
广泛的类型,在定义一个类的时候,类型中有些方法参数、返回值类型不确定,可以使用一个符号,来表示那些尚未确定的类型,这个符号,就称为泛型。泛型是java5中引入的特性,它提供了编译时类型安全检测机制
在集合当中,我们去创建集合对象的时候,后面可以添加一个<引用数据类型> ,表示泛型,表示的是该集合当中,存储的元素的数据类型。在集合的创建中添加了泛型之后,该集合只能存储该类型的数据,或者是该类型子类的数据,不能存储不是该类型,并且不能用该类型接受的数据。 <>特别像菱形,称为“菱形泛型”,jdk1.7特性
泛型类的定义:
- 说明:类名后面跟着的泛型类型,是泛型的声明,一旦泛型声明出来,就相当于这个类型成为了已知类型,这个类型就可以在整个类中使用
- 泛型的声明名称,只需要是一个合法的标识符即可,但是通常我们使用单个大写字母来表示,常用字母:T(type)、W、Q、K(key)、V(value)、E(element)
- 泛型确定的时机:将来在使用这个类,和创建对象的时候
泛型方法的定义:
静态方法只能使用自己定义泛型(类上泛型的具体类型需要创建对象才能确定, 静态优先于对象存在, 不能使用类上泛型)
- 在方法上声明的泛型,可以在整个方法中,当做已知类型来使用
- 如果【非静态】方法上没有任何泛型的声明,那么可以使用类中定义的泛型
- 如果【静态】方法上没有任何的泛型声明,那么就不能使用泛型,连类中定义的泛型,也不能使用,因为类中的泛型需要在创建对象的时候才能确定。所以【静态】方法想使用泛型,就必须在自己的方法上单独声明。
泛型通配符:使用泛型的时候,没有使用具体的泛型声明T,而是使用了和声明过的某个泛型T有关的一类类型,就称为泛型的通配符。 三种形式
-
第一种形式,使用?来表示可以是任意类型,例如:
Collection接口中的removeAll(Collection<?> c),表示可以接收任意泛型类型的集合,作为该方法的实际参数,参数集合的泛型,可以是与E没有任何关系
-
第二种形式,使用? extends E来表示必须是某个泛型类型或是该泛型类型的子类,例如:
Collection接口中的addAll(Collection<? extends E> c),表示可以接收泛型类型是调用者泛型类型或者其子类的集合,作为该方法的实际参数。参数的泛型和调用者的泛型,必须有关(相同或者是子父类)。确定了泛型的上边界。
-
第三种形式,使用? super E来表示必须是某个泛型类型或者是该泛型类型的父类,例如:
Arrays工具类中,排序方法static void sort(T[] a, Comparator<? super T> c),T是该方法的泛型,T表示的是数组中元素的类型,<? super T>表示可以接收泛型类型是数组元素类型T或者是元素类型T的父类的比较器,作为sort方法的参数。参数的泛型和方法的泛型,必须有关(相同或者是子父类)。确定了泛型的下边界。
无序单列集合Set
与List接口不同的是,Set接口中元素无序,并且都会以某种规则保证存入.无序、不可重复(没有位置的区分,相同值的元素没有任何分别)、没有索引
Set集合遍历方式:迭代器遍历、增强for遍历。
数据结构之哈希表 (1.8之前 数组 + 链表)
哈希值:是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值,Object类中的public int hashCode()返回对象的哈希码值。特点如下:
-
同一个对象多次调用hashCode()方法返回的哈希值是相同的
-
默认情况下,不同对象的哈希值是不同的。而重写hashCode()方法,可以实现让不同对象的哈希值相同
HashSet保证元素唯一源码分析:
- 某个对象obj,在即将要存储到HashSet集合的时候,首先计算obj的hashCode值
- 在集合中的所有元素的哈希值,都和obj的哈希值不同,说明在集合中不存在obj,可以直接将obj存储到HashSet中
- 在集合中有若干元素的哈希值,和obj的哈希值相同,并不能说明obj已经存在于集合中,需要使用equals判断obj是否和那些与自己哈希值相同的元素是否相等
- 如果这些元素所有的和obj比较equals之后,都不相等,那么就说明obj不存在于集合中,可以将obj存储到HashSet中
- 如果这些元素有任意一个和obj比较equals之后,发现相等,那么就说明obj已经存在于集合中,所以obj就不能存储,存储失败
LinkedHashSet(扩展)
LinkedHashSet是HashSet的子类, 与父类在功能上完全一致, 实现与 HashSet 的不同之外在于,LinkedHashSet维护着一个运行于所有条目的双重链接列表。此链接列表定义了元素迭代顺序(如果即想元素唯一, 又想元素存取有序, 可以使用LinkedHashSet)。唯一 、 有序
Map集合 双列集合的顶层接口
一个数据(key)到另一个数据(value)的映射关系(对应关系)。
- Map<K,V>的特点:称为键值对映射关系(一对一)
Key(键)是唯一的(不重复),value(值)不是唯一的
每个键都只能对应确定唯一的值 ;Map集合没有索引,因此存储的元素不能保证运行。
HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
Map接口中常用的方法:
-
public V put(K key, V value): 如果添加的key值在map集合中不存在, 那么put方法表示将键值对添加到map集合中; 如果添加的key值在map集合中存在, 那么put方法表示修改value值。
-
public V remove(Object key): 把指定的键所对应的键值对元素在Map集合中删除,返回被删除元素的值。
-
public V get(Object key):根据指定的键,在Map集合中获取对应的值。
-
boolean containsKey(Object key): 判断集合中是否包含指定的键。
-
void clear() : 表示清空Map集合中所有键值对数据
-
boolean isEmpty() : 验证Map集合中是否还是键值对数据, 如果没有返回true, 如果有返回false
-
int size() : 获取到Map集合中键值对数据个数(求Map集合的长度)
Map集合的遍历方式
键遍历:获取Map集合中的所有键,放到一个Set集合中,遍历该Set集合,获取到每一个键,根据键再来获取对应的值。 Set keySet()====》遍历Set集合两种方式 迭代器、增强for;
- 获取map集合中所有key值到set集合中
- 遍历set集合(迭代器,foreach)
// 1. 获取map集合中所有key值到set集合中
Set<Integer> set = map.keySet();
// 2. 遍历set集合(迭代器,foreach)
for(Integer key : set){
String value = map.get(key);
System.out.println(key + "--" + value);
}
键值对遍历:
- 获取Map<K,V>集合中,所有的键值对(Entry)对象,以Set集合形式返回。方法:entrySet()。
- 遍历包含键值对(Entry)对象的Set集合,得到每一个键值对(Entry)对象。
- 通过键值对(Entry)对象,获取Entry对象中的键与值。方法:getkey() getValue()
// 1. 获取Map集合中所有的键值对,放置在Set集合
Set<Map.Entry<Integer,String>> set = map.entrySet();
// 2. 遍历set集合,获取出每一对
for(Map.Entry<Integer,String> entry : set){
Integer key = entry.getKey();
String value = entry.getValue();
System.out.println(key + "~~" + value);
}
// 使用迭代器进行Set集合遍历
Iterator<Map.Entry<Integer,String>> it = set.iterator();
while(it.hasNext()){
Map.Entry<Integer,String> e = it.next();
System.out.println(e.getKey() + "==" + e.getValue());
}
HashMap中key值唯一分析
-
HashMap就是Map集合使用哈希表的存储方式的一种实现类
-
HashMap存储的是jdk中提供的类型的键,就可以直接保证键的唯一性
-
HashMap中存储的键,是自定义类型,无法保证键的唯一性;原因:虽然都是张三、23,但是这些对象并不是相同的对象,这些对象的哈希值计算结果各不相同,就说明一定不是相同的对象,所以无法保证键的唯一。重写hashCode和equals方法
说明:HashMap的键的唯一性和HashSet的元素的唯一性,保证方式都一样
HashMap和HashSet的关系:
1、HashSet是由HashMap实现出来的,HashSet就是HashMap的键的那一列
2、将HashMap中的值的那一列隐藏掉,就变成了HashSet
- LinkedHashMap是HashMap的子类, 哈希表和链接列表实现,此实现与 HashMap 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表, 具有可预知的迭代顺序
Collections工具类
可变参数的使用和注意事项
- 概述: 可变参数又称参数个数可变,用作方法的形参出现,那么方法参数个数就是可变的了,方法的参数类型已经确定,个数不确定,我们可以使用可变参数.
- 格式:
修饰符 返回值类型 方法名(数据类型…变量名) {
}
注意事项:
- 可变参数在方法中其实是一个数组
- 如果一个方法有多个参数,包含可变参数,可变参数必须要放在最后
- 方法中只能有一个可变参数
Collections单列集合工具类
Collections类是一个单列集合的工具类,在类中封装类很多常用的操作集合的方法.因为Collections工具类中, 没有对外提供构造方法, 因此不能创建对象, 导致类中所有成员和方法全部静态修饰, 类名.直接调用。
-
sort(List list): 将指定的列表按升序排序,从小到大
-
max、min(Collection c):获取集合的最大值或者最小值
-
replaceAll(List list, E oldVal, E newVal):将集合list中的所有指定老元素oldVal都替换成新元素newVal
-
reverse(List list):将参数集合list进行反转
-
shuffle(List list):将list集合中的元素进行随机置换
File类 文件和目录路径名的抽象表示
文件和目录是可以通过File封装成对象的,对于File而言,其封装的并不是一个真正存在的文件,仅仅是一个路径名而已。这个路径名它可以是存在的,也可以是不存在的.将来是可以通过具体的操作把这个路径的内容转换为存在的路径操作。
路径分类:绝对路径(从盘符开始)、相对路径(相对当前项目下的路径)。
构造方法:
- File(String path):把字符串的路径,封装成一个File对象。
- File(String parent, String child):将父级路径和子级路径封装成一个File对象,其实描述的是父级路径和子级路径拼接后的路径。
- File(File parent, String child):将父级File对象路径和字节路径封装成一个File对象,其实描述的也是父级路径和子级路径拼接后的路径。
File类型的创建方法:
- boolean createNewFile():创建当前File对象所描述的路径的文件
- boolean mkdir():创建当前File对象所描述的路径的文件夹(如果父级路径不存在,那么不会自动创建父级路径)mkdir: makedirectory 创建目录(创建文件夹)
- boolean mkdirs():创建当前File对象所描述的路径的文件夹(如果父级路径不存在,那么自动创建父级路径)
File类型的删除方法:
- delete():删除调用者描述的文件或者文件夹, 文件存在或者文件夹为空才能删除成功
- delete在删除文件夹的时候,只能删除空文件夹, 是为了尽量保证安全性
- delete方法不走回收站
File类型常用的判断功能:
- exists():判断当前调用者File对象所表示文件或者文件夹,是否真实存在, 存在返回true,不存在返回false
- isFile():判断当前调用者File对象,是否是文件
- isDirectory():判断当前调用者File对象,是否是文件夹
File类型的获取功能:
- String getAbsolutePath():获取当前File对象的绝对路径
- String getPath():获取的就是在构造方法中封装的路径
- String getName():获取最底层的简单的文件或者文件夹名称(不包含所造目录的路径)
- String[] list():获取当前文件夹下的所有文件和文件夹的名称,到一个字符串数组中
- File[] listFiles():获取当前文件夹下的所有文件和文件夹的File对象,到一个File对象数组中
IO流
常见的应用: 文件复制、文件上传、 文件下载等;是一种抽象概念,是对数据传输的总称.也就是说数据在设备间的传输称为流。
- 按照数据的流向:
输入流(input):读数据
输出流(output):写数据
- 按照数据类型:
字节流:(图片,视频…)
字节输入流和字节输出流
字符流:(操作中文 GBK–>2字节 UTF-8–>3字节)
字符输入流和字符输出流
- 字节流和字符流的使用场景:
如果操作的是纯文本文件,优先使用字符流
如果操作的是图片、视频、音频等二进制文件,优先使用字节流
如果不确定文件类型,优先使用字节流.字节流是万能的流
注意事项:
(1) 在操作之前,要导包,java.io包
(2) 在操作流对象的时候,要处理解决异常(IOException)
(3) 在操作完流对象之后,必须关闭资源, 所有流资源的关闭 close();
字节流
(1) InputStream:这个抽象类是表示字节输入流所有类的超类
(2) OutputStream:这个抽象类是表示字节输出流所有类的超类
字节输入流FileInputStream 磁盘交互 读取1个字节或者多个字节
FileInputStream(File f):将一个File对象所表示的文件路径封装在一个字节输入流中
FileInputStream(String path):将一个字符串所表示的文件路径封装在一个字节输入流中
int read():从当前的字节输入流中,读取并返回一个字节,返回值结果int类型, 表示读取到的字节对应的整数结果, 如果返回-1表示证明文件读取完毕
int read(byte[] arr):从当前的字节输入流中最多读取arr.length个字节,读取到的字节放置到参数arr数组中,返回值结果int类型, 表示本次读取到的字节个数, 如果读到-1,证明文件读取完毕 数组读取效率优于单个字节读取效能
void close():关闭该流对象
一个一个字节读取
// len表示每次读取到的字节结果
int len;
while((len = fis.read()) != -1){
System.out.println((char)len);
}
// 4. 关闭资源
fis.close();
按照字节数组读取
// len表示每次读取到的字节个数
long begin = System.currentTimeMillis();
int len;
while((len = fis.read(arr)) != -1){
System.out.print(new String(arr,0,len));
} long end = System.currentTimeMillis();
System.out.println(end-begin);
fis.close();
字节输出流FileOutputStream 磁盘交互
FileOutputStream(File f):将f描述的路径文件封装在字节输出流对象中
FileOutputStream(String path):将path描述的文件路径封装在字节输出流对象中
FileOutputStream(String path,boolean append):如果第二个参数为true,则字节将写入参数path对应文件的末尾而不是开头(追加写入)
FileOutputStream(File path,boolean append):如果第二个参数为true,则字节将写入参数path对应文件的末尾而不是开头(追加写入)
字节流写数据的方式:
- void write(int b): 将指定的字节写入此文件输出流一次写一个字节数据
- void write(byte[] b): 将b.length字节从指定的字节数组写入此文件输出流
- void write(byte[] b, int off, int len): 把b数组的off索引开始的len个字节通过字节输出流写出
文件的拷贝
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo05_图片复制 {
public static void main(String[] args) throws IOException {
// 1. 创建出一个输入流(读文件),绑定数据源
FileInputStream fis = new FileInputStream("D:\\0307Java\\day05\\空指针异常.png");
// 2. 创建出一个输出流(向文件中写入), 绑定数据目的
FileOutputStream fos = new FileOutputStream("D:\\异常cpoy.png");
// 3. 进行文件复制
// 通常,数组大小一般都是1024倍数, 最常见的数组大小 1024 1024*4 1024*8
byte[] b = new byte[1024*4];
// len表示每次读取到的字节个数
int len;
while((len = fis.read(b)) != -1){
fos.write(b,0,len);
} fis.close();
fos.close();
}
}
缓冲字节流
BufferedOutputStream:是OutputStream的子类, 表示高效字节输出流。流资源在创建时, 提供默认的数组缓冲区, 按照缓冲区数组大小进行写入, 实现效率
BufferedInputStream:是InputStream的子类, 表示高效字节输入流。资源在创建时, 提供默认的数组缓冲区, 按照缓冲区数组大小进行读取, 实现效率
这两个流是包装类型:本身不具备读写的功能,只是在某个具体的流对象的基础上,对其进行加强,例如FileInputStream和FileOutputStream,原本效率较低,加强之后,就效率较高。
构造方法:
-
BufferedOutputStream(OutputStream out): 创建字节缓冲输出流对象
BufferedOutputStream高效的原理:在该类型中准备了一个数组,存储字节信息,当外界调用write方法想写出一个字节的时候,该对象直接将这个字节存储到了自己的数组中,而不刷新到文件中。一直到该数组所有8192个位置全都占满,该对象才把这个数组中的所有数据一次性写出到目标文件中。如果最后一次循环过程中,没有将数组写满,最终在关闭流对象的时候,也会将该数组中的数据刷新到文件中。
-
BufferedInputStream(InputStream in): 创建字节缓冲输入流对象
BufferedInputStream高效的原理:在该类型中准备了一个数组,存储字节信息,当外界调用read()方法想获取一个字节的时候,该对象从文件中一次性读取了8192(1024*8)个字节到数组中,只返回了第一个字节给调用者。**将来调用者再次调用read方法时,当前对象就不需要再次访问磁盘,只需要从数组中取出一个字节返回给调用者即可,由于读取的是数组,所以速度非常快。**当8192个字节全都读取完成之后,再需要读取一个字节,就得让该对象到文件中读取下一个8192个字节了。
flush()方法 和 close()方法的区别:
- close方法会先调用flush方法
- close方法用于流对象的关闭,一旦调用了close方法,那么这个流对象就不能继续使用了
- flush只是将缓冲区中的数据,刷新到相应文件中,而不会将流对象关闭,可以继续使用这个流对象。但是如果flush方法使用过于频繁,那么丧失了缓冲区的作用。
IO中保证流对象关闭的标准格式
捕获异常 和 声明异常的原则:
如果知道如何处理异常,那么就使用try…catch来进行处理;如果不知道如何处理异常,那么就使用throws来对异常进行声明或者将当前的异常包装成其他的异常类型,进行声明,因为越到代码的高层,拥有更多的资源、更高的位置和权力,知道如何处理,底层被调用的方法,虽然是出现异常的位置,但是并不知道如何处理。
如果你希望程序出现异常之后,继续运行下去,那么就使用try…catch;如果出现异常之后,希望当前方法的代码停止运行,那么就使用throws。
try{
流对象的使用;
}catch(异常类名 变量名){
异常的处理代码;
}finally{
关闭流资源;
}
1. 从JDK1.7版本, 有针对于流资源处理的新语法结构
try(
流资源的创建;
){
流资源使用过程;
}catch(异常声明 ex){
异常处理;
}
使用字节流读写字符的话:
- 使用字节流写字符 【乱码】
可以使用,但是需要先把字符串转成字节数组,再存储到文件中,比较麻烦
- 使用字节流读取字符
如果是纯英文,可以一次读取一个字节
**如果是纯中文,可以一次读取两个字节(GBK)**
**如果是中英文混杂,每次不知道读取多少个字节,因此无论字节数组准备多大,都会出现乱码**
字节流读取带有中文的文本文件时, 因为读取最小粒度单个字节,一个中文在不同的编码表中占有的字节位数不同
ASCII: 0-127中的数字, 符号, 字母大小写, 都是一字节, ASCII是被所有编码表兼容
GBK: 中国标准编码表,包含有ASCII(0-127), 也同时包含有中文文字, 一个中文在GBK编码表中占有2个字节大小
UTF-8: 万国编码表, 兼容了各个国家语言文字, 中文包含在内, 兼容ASCII;一个中文在UTF-8编码下, 占有3个字节;
字符输出流
Writer: 用于写入字符流的抽象父类
FileWriter: 用于写入字符流的常用子类
FileWriter字符流的构造方法:
- FileWriter(File file): 根据给定的 File 对象构造一个 FileWriter 对象
- FileWriter(File file, boolean append): 根据给定的文件名以及指示是否附加写入数据的 boolean 值来构造 FileWriter 对象
- FileWriter(String fileName): 根据给定的文件名构造一个 FileWriter 对象
- FileWriter(String fileName, boolean append)
根据给定的文件名以及指示是否附加写入数据的 boolean 值来构造 FileWriter 对象
方法:
- void write(int c): 写一个字符
- void write(char[] cbuf): 写入一个字符数组
- void write(char[] cbuf, int off, int len): 写入字符数组的一部分
- void write(String str): 写一个字符串
- void write(String str, int off, int len): 写一个字符串的一部分
字符输入流
Reader: 用于读取字符流的抽象父类
FileReader: 用于读取字符流的常用子类
构造方法:
- FileReader(File file): 在给定从中读取数据的 File 的情况下创建一个新 FileReader
- FileReader(String fileName): 在给定从中读取数据的文件名的情况下创建一个新 FileReader
读的方法:
- int read(): 一次读一个字符数据,返回值结果是int类型, 返回的就是这个字符在编码表中对应的整数结果,如果读到-1,证明文件读取完毕
- int read(char[] ch): 一次最多读一个字符数组数据, 将读取出的字符放置在参数ch数组中, 返回值结果int类型, 表示每次从文件中读取到的字符的个数, 如果读取到-1,证明文件读取完毕
字符流复制结论:
- 字符流,可以复制纯文本文件, 但是效率不高, 因此对于文件的复制,通常采用字节流
- 字符流,不能复制非纯文本文件(复制的内容会出现问题)
字符流,读写文件,必须要将字节参考编码表转换成字符;非文本文件,不能通过编码表转换,导致了赋值的错误
字符缓冲流
BufferedWriter:将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入,可以指定缓冲区大小,或者可以接受默认大小。默认值足够大,可用于大多数用途。
BufferedReader:从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取,可以指定缓冲区大小,或者可以使用默认大小。 默认值足够大,可用于大多数用途。
构造方法:
- BufferedWriter(Writer out): 创建字符缓冲输出流对象
BufferedWriter:每次调用write方法,不会直接将字符刷新到文件中,而是存储到字符数组中,等字符数组写满了或者flush刷新缓冲区,才一次性刷新到文件中,减少了和磁盘交互的次数,提升了效率。
-
BufferedReader(Reader in) : 创建字符缓冲输入流对象
BufferedReader:每次调用read方法,第一次从磁盘中读取了默认数组缓冲大小个字符,存储到该类型对象的缓冲区数组中,将其中一个返回给调用者,再次调用read方法时,就不需要再访问磁盘,直接从缓冲区中拿一个出来即可,效率提升了很多。
高效缓冲字符流特有的方法:
-
BufferedReader:
readLine():可以从输入流中,一次读取一行数据,返回一个字符串,如果到达文件末尾,则返回null
-
BufferedWriter:
newLine():写一行行分隔符.
转换流
针对于不同编码表的文件, 通过字符流进行读写会发生乱码问题, 为了正确文本文件读写过程, 使用转换流;将GBK编码的文本文件内容, 复制到UTF-8编码的文件中。例如将GBK编码的文本文件内容, 复制到UTF-8编码的文件中。
转换流的使用:
-
OutputStreamWriter:Writer的子类, 字符流到字节流的桥梁,可以指定编码形式
构造方法:OutputStreamWriter(OutputStream os, String charSetName)
创建一个转换流对象,可以把将来方法中接收到的字符,通过指定的编码表charSetName,编码成字节信息,再通过指定的字节流os,将字节信息写出。
-
InputStreamReader:Reader的子类, 字节流到字符流的桥梁,可以指定编码形式
构造方法:InputStreamReader(InputStream is, String charSetName)
创建一个转换流对象,可以使用is这个指定的字节流,从磁盘中读取字节信息,通过指定的编码表charSetName,将字节信息解码成字符信息,返回给调用者。
import java.io.*;
public class Demo02_转换流解决中文不同编码乱码 {
public static void main(String[] args) throws IOException {
// 注意: 使用转换流时, 读文件还是写文件, 提供的编码集与正在操作的文件对应的编码必须一致
InputStreamReader isr = new InputStreamReader(
new FileInputStream("day19\\GBK.txt"),"GBK"
);
OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("day19\\UTF-8.txt"),"UTF-8"
);
int len;
while((len = isr.read()) != -1){
osw.write(len);
}
isr.close();
osw.close();
}
}
对象序列化
-
对象序列化:就是将对象保存到磁盘中,或者在网络中传输对象 。
使用一个字节序列表示一个对象,该字节序列包含:对象的类型、对象的数据和对象中存储的属性等信息,字节序列写到文件之后,相当于文件中持久保存了一个对象的信息(把对象存储在文件中)。
-
对象反序列化: 将序列化的字节序列从文件中读取回来,重构对象(把文件当中存储的对象读取出来)。
对象序列化流:ObjectOutputStream ObjectInputStream
对象序列化流的使用
ObjectOutputStream : 对象序列化流,是OutputStream的子类,将Java对象的原始数据类型和图形写入OutputStream[输出流]
**构造方法:**ObjectOutputStream (OutputStream o): 创建一个写入指定的OutputStream的ObjectOutputStream ;
**常用方法:**writeObject(Object obj): 将指定的对象写入ObjectOutputStream;
注意事项:
-
一个对象要想被序列化,该对象所属的类必须必须实现Serializable 接口
-
Serializable是一个标记接口,实现该接口,不需要重写任何方法; 才具有序列化和反序列化的功能
对象反序列化流的使用
ObjectInputStream : 对象反序列化流,是InputStream的子类,就是将持久化保存的对象,按照字节顺序进行读取,ObjectInputStream反序列化先前使用ObjectOutputStream编写的原始数据和对象;
**构造方法:**ObjectInputStream(InputStream in): 创建从指定的InputStream读取的ObjectInputStream
**常用方法:**readObject(): 从ObjectInputStream读取一个对象, 返回值类型为Object;
EOFException : End Of File Excepttion 文件到达结束位置,不能再进行对象的获取了
问题:反序列时,将对象从文件中读取,不知道文件中有几个对象, 于是获取对象次数多了, 报出EOFException, 文件到达末尾异常,怎么解决?
优化方案: 将多个对象放置到一个集合中, 将这个集合对象写入到文件中,实现序列化过程; 读取文件的时候,就读一次, 将集合对象获取出来, 遍历集合相当于将集合中所有对象都获取到. 如此可以避免掉,不知道文件中有多少对象从而报出的异常问题
import java.io.*;
import java.util.ArrayList;
public class Demo04_对象序列化优化 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
writeArrayList();
readArrayList();
}
public static void writeArrayList() throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("day19\\personList.txt")
);
ArrayList<Person> list = new ArrayList<>();
list.add(new Person("zs",20));
list.add(new Person("李四",19));
list.add(new Person("王五",21));
oos.writeObject(list);
oos.close();
}
public static void readArrayList() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("day19\\personList.txt")
);
ArrayList<Person> list = (ArrayList<Person>)ois.readObject();
for(Person p : list){
System.out.println(p.getName() + "--" + p.getAge());
}
ois.close();
}
}
序列号serialVersionUID和transient关键字
-
serialVersionUID:序列化了一个对象后,假如我们修改了对象所属的类文件,读取数据会不会出问题呢?会出问题,会抛出InvalidClassException异常。
-
解决方案: 重新序列化 || 给对象所属的类加一个serialVersionUID
private static final long serialVersionUID = 42L;
transient:
如果一个对象中的某个成员变量的值不想被序列化,又该如何实现呢?
给成员变量加transient关键字修饰,该关键字标记的成员变量不参与序列化过程。
Properties
Properties:表示一个持久的属性集,是一个Map体系的集合类,是Hashtable的子类,所以可以当做普通的Map来使用,属性列表中的每个键及其对应的值都是一个字符串,因此不需要写泛型。一般是用于进行配置文件的读写,键值对的关系。
直接使用空参构造: Properties p = new Properties();
Properties 集合常用方法:
- put(key k,Value v): 将参数中的键值对存储在Properties集合中,继承自父接口中的方法
- setProperty(String key, String value) : 将键值对映射关系添加到集合中,键和值都是String类型,key值不重复,如果重复,表示value值修改
- getProperty(String key): 通过指定key值获取到对应value
- stringPropertyNames() :将Properties集合中所有的key值获取到,放置到Set集合中,返回类型set
Properties和IO流结合使用
- load(InputStream inStream) : 从输入字节流读取属性列表(键和元素对)
- load(Reader read): 从输入字符流读取属性列表(键和元素对)
- store(OutputStream out, String comments) : 使用字节的输出流,将Properties集合中的键值对元素同步到对应的文件中,comments 就是针对于本次修改的描述
- store(Writer out, String comments) : 使用字符的输出流,将Properties集合中的键值对元素同步到对应的文件中,comments 就是针对于本次修改的描述
单例模式
在整个系统中,一个类型,只有一个对象,是有场景下使用的都是同一个对象;
单例模式设计思路:
- 私有构造方法 Math,System,Arrays,Collections 构造方法私有化
- 在本类中创建出一个唯一的私有对象
- 为其他的外类提供唯一对象的公共方法方式
饿汉式
定义一个类型, 这个类型一旦加载,马上将这个类型的唯一对象给你;
类型的唯一对象马上就要给出,不能等待,非常饥渴,称为饿汉式;
实现步骤: 可以赋值 为空null,则 没有单例了
- 构造方法私有
- 在当前类中创建出唯一的一个对象, 使用private static 修饰
- 提供对外的访问唯一对象的公共方式
//饿汉式实现单例模式
public class SingletonHunury {
// 1. 构造方法私有化: 不让外界创建对象
private SingletonHunury(){}
// 2. 本类中创建出唯一的对象
// 1) 因为类构造私有化,不能创建对象, 于是类中的一切成员必须是static修饰的,外界才能使用
// 类名.直接调用
// 2) 私有: 外界可以直接访问s对象, 能给其赋值为null, 没有单例了, 为了不让外界直接访问
// 添加了private修饰
private static SingletonHunury s = new SingletonHunury();
// 3. 为唯一的成员对象s提供对外的公共访问方式
public static SingletonHunury getInstance(){
return s;
}
}
老汉式
私有构造
创建一个唯一对象方式 : 类中只有一个对象,并且还是public static final修饰;
// 单例模式老汉式实现
public class SingletonOldMan {
// 1. 构造方法私有化
private SingletonOldMan(){}
// 2. 创建出一个唯一对象
public final static SingletonOldMan som = new SingletonOldMan();
}
懒汉式
public class SingletonLazy {
// 1. 构造方法私有化
private SingletonLazy(){}
// 2. 成员变量位置声明出唯一的对象
// static: 构造方法私有, 外界不能进行对象创建, 类中的一切成员都需要静态修饰, 用类名.直接调用
// private: 不想外界直接调用,安全隐患, 调用可以赋值为null
private static SingletonLazy sl;
// 3. 对外的公共的获取sl对象方式+
// 假设 A B C 三个线程需要使用SingletonLazy唯一对象
public static SingletonLazy getInstance(){
// 该判断为了让不创建对象的使用, 直接返回唯一的成员对象sl, 为了解决同步代码块每次验证性能低情况
if(sl == null){
// 只有第一次创建对象需要进入到同步代码块中, 因为需要保证, 创建一个对象的同时, 代码完整执行完毕的
synchronized (SingletonLazy.class){// 性能低,浪费时间
if(sl == null){
// A对应的CPU资源没了
// B对应进入方法中执行, B创建出一个对象
sl = new SingletonLazy();
}
}
}
return sl;
}
}
多线程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:**线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。**一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
并行parallel:多个任务(进程、线程)同时运行。在某个确定的时刻,有多个任务在执行,条件:有多个cpu,多核编程;
并发concurrent:多个任务(进程、线程)同时发起。不能同时执行的(只有一个cpu),只能是同时要求执行。就只能在某个时间片内,将多个任务都有过执行。一个cpu在不同的任务之间,来回切换,只不过每个任务耗费的时间比较短,cpu的切换速度比较快,所以可以让用户感觉就像多个任务在同时执行。(实际开发中的多线程程序,都是并发运行机制。
多线程的实现方式
继承方式
-
继承Thread类
-
步骤:
-
定义一个类,继承Thread类
-
重写自定义类中的run方法,用于定义新线程要运行的内容
-
创建自定义类型的对象
-
调用线程启动的方法:start方法
-
使该线程开始执行;Java 虚拟机调用该线程的 run 方法
// 1. Thread类表示线程类, 可以在代码中实现多线程
public class MyThread extends Thread {
// 2. 重写run方法: 没有返回值结果, 没有异常声明, 重写后的方法中的异常, 使用try...catch处理
// 当独立的线程通道开启后, 执行的就是run方法中内容
@Override
public void run(){
for (int i = 1; i <= 10; i++){
System.out.println("run----" + i);
}
}
}
实现方式
实现Runnable接口:Runnable接口的实现类对象,表示一个具体的任务,将来创建一个线程对象之后,让线程执行这个任务
步骤:
-
定义一个任务类,实现Runnable接口
-
重写任务类中的run方法,用于定义任务的内容
-
创建任务类对象,表示任务
-
创建一个Thread类型的对象,将任务对象作为构造参数传递,用于执行任务类对象
Thread(Runnable able);
调用线程对象的start方法,开启新线程;调用的就是Thread类构造方法中传递的able线程任务中的run方法
public class MyRunnable implements Runnable{
// 将需要独立运行的代码设计在run中
@Override
public void run() {
for(int i = 1; i <= 10; i++){
System.out.println("runnable----" + i);
}
}
实现Callable接口
- V call(): 计算结果,如果无法计算结果,则抛出一个异常
- FutureTask(Callable callable): 创建一个 FutureTask,一旦运行就执行给定的 Callable
- V get(): 如有必要,等待计算完成,然后获取其结果
实现步骤:
- 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
- 创建MyCallable类的对象
- 创建Runnable的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 使用Thread类的对象调用start方法启动线程
- FutureTask对象用get方法,就可以获取线程结束之后的结果。
import java.util.concurrent.Callable;
// 1. 自定义一个类, 作为Callable实现类
// Callable接口上的泛型, 表示call方法的返回值类型
public class MyCallable implements Callable<Integer> {
// 2. 重写唯一抽象方法call: 对比run, 有返回值结果, 可以抛出异常
@Override
public Integer call() {
int i = 1;
for(; i <= 10; i++){
System.out.println("call---" + i);
}
return i;
}
}
线程测试类:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo01_TestThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// main方法本身就是一个线程: 主线程, 在代码中除了main方法之外, 再创建出一条独立的代码执行通道(在开启一条线程)
// 3. 创建一个自定义线程类对象
MyThread my = new MyThread();
// 4. 调用从父类Thread中继承到的start方法()
// 1) 开启线程通道
// 2) 主动调用当前线程对象中的run方法
my.start();
// 使用Runnable实现多线程
MyRunnable my2 = new MyRunnable();
// Thread t2 = new Thread(Runnable run)
// t2调用start方法, 执行的是参数my2中的run方法
Thread t2 = new Thread(my2);
t2.start();
// 使用Callable实现多线程
MyCallable my3 = new MyCallable();
// 问题: Callable --->FutureTask---->Thread
// FutureTask(Callable call): 是Runnable一个实现类
// Thread t3 = new Thread(FutureTask);
FutureTask<Integer> ft = new FutureTask<>(my3);
Thread t3 = new Thread(ft);
t3.start();
for(int i = 1; i <= 10; i++){
System.out.println("main---" + i);
}
// FutureTask中有get方法, 将Callable中的call结果获取到
System.out.println(ft.get() + "-----------------");
}
}
Thread线程常用方法
线程API之线程名称
- Thread(Runnable r , String name): 传递一个Runnable的任务对象,还可以传递一个name的线程名称
- void setName(String name): 将此线程的名称更改为等于参数name
- String getName(): 返回此线程的名称
- static Thread currentThread(): 返回对当前正在执行的线程对象的引用,哪个线程执行这行代码,就返回哪个线程
线程API之线程休眠
static void sleep(long millis): 使当前正在执行的线程停留(暂停执行)指定的毫秒数;
说明: 参数是毫秒值, 1000毫秒 = 1秒, 当线程执行到了sleep方法,休眠指定的毫秒数, 在休眠时间段内, 不再竞争CPU资源, 当休眠时间结束, 可以继续进行CPU资源竞争;
应用场景:日终批量(其中有一个功能,日终对账, 每天3:00进行上一日资金对账, 第三方提供一个文件在指定的目录下, 程序需要在3:00, 到这个路径下下载文件(读取文件内容), 假如文件不存在, 给三次机会反复进行文件判断是否存在验证, 每一次机会中,让代码停止1秒钟,进行缓冲等待)
线程API之线程优先级
final int getPriority(): 返回此线程的优先级;
final void setPriority(int newPriority): 更改此线程的优先级,线程默认优先级是5;线程优先级的范围是:1-10
Thread类提供的优先级常量(静态常量)
MIN_PRIORITY = 1; 最低
NORM_PRIORITY = 5; 默认
MAX_PRIORITY = 10; 最高
线程API之线程礼让
static void yield(): 没有绝对性
线程让步,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,忽略此方法;
线程API之后台线程
后台线程也叫作守护线程。守护是用来提供服务的;特点:如果一个程序的运行中,没有非守护线程了,只有守护线程了,那么守护线程会过一段时间后自动停止。
final void setDaemon(boolean on) : 参数设置为true,将线程设置为守护线程
将此线程标记为daemon线程或用户线程。当运行的唯一线程都是守护进程线程时,Java虚拟机将退出
线程安全问题
线程执行的随机性导致的安全问题,线程卖票过程中随时可能丢失cpu的执行权,导致当前线程没有执行完就因为没有资源而被迫暂停, 当资源再次回归该线程时,代码中的数据可能可能已经与之前不同, 因此出现了数据安全问题;
解决方案:保证线程中代码执行的完整性, 原子性. 原子操作就是不可分割的操作,例如售票的过程中的代码就是一个不可分割的操作。
同步代码块:
这种格式,可以确保cpu在执行A线程的时候,不会切换到影响A线程执行的其他线程上去;
使用格式:
synchronized (锁对象) {
需要保证完整性、原子性的代码;
}
1. synchronized : 同步的概念,关键字
2. 小括号中,可以设计任意一个对象, 但是保证是多个线程共享的唯一对象, 这个对象的作用就像一把锁, 将同步代码块中的代码锁住不被其他线程打扰
3. 将需要保证完整执行的代码,设计在同步代码块中
使用同步代码块之后的效果:
**当cpu想去执行同步代码块的时候,需要先获取到锁对象,获取之后就可以运行代码块中的内容;当cpu正在执行当前代码块的内容时,cpu可以切换到其他线程,但是不能切换到需要相同锁对象的线程上。**
**当cpu执行完当前代码块中的代码之后,就会释放锁对象,cpu就可以运行其他需要当前锁对象的同步代码块了**
缺点:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
同步代码块的知识点:
- 同步代码块: 因为每次执行都需要 判断锁 获取锁 归还锁, 三个动作, 因此带有同步安全机制的代码, 性能相对比较低
- 大量字符串拼接时, 解决字符串浪费常量池空间的问题
StringBuilder和StringBuffer区别:
- StringBuilder起始于JDK1.5版本, StringBuffer起始于JDK1.0版本
- StringBuilder线程不安全, 效率高; StringBuffer线程安全, 效率低;
同步方法
同步成员方法:
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
同步静态方法:
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
1. 两种同步方法的锁对象
2. 同步成员方法的锁对象是 this
3. 同步静态方法的锁对象是 类名.class
Lock锁 JDK5以后提供了一个新的锁对象Lock
Lock 提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作
Lock锁提供了获取锁和释放锁的方法:
- void lock(): 获得锁
- void unlock(): 释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。
ReentrantLock构造方法: ReentrantLock()创建一个ReentrantLock的实例
import java.util.concurrent.locks.ReentrantLock;
public class SaleTicketsLock implements Runnable{
// 1. 剩余票数设计成成员变量
static int tickets = 55;
ReentrantLock l = new ReentrantLock();
// 2. 买票的过程
@Override
public void run() {
while(tickets > 0){
// 为代码添加锁对象
l.lock();
// synchronized (obj){
if(tickets > 0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在销售第"
+ tickets-- + "张票");
}
// 释放锁对象
l.unlock();
// }
}
}
public static void main(String[] args) {
// 创建出三个线程模拟三条渠道
SaleTicketsLock st = new SaleTicketsLock();
Thread t1 = new Thread(st,"团团");
Thread t2 = new Thread(st,"猫猫");
Thread t3 = new Thread(st,"影院");
t1.start();
t2.start();
t3.start();
}
}
死锁现象
死锁的发生: 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程均处于等待状态,无法前往执行。
线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。
线程池
系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,**当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务本身资源的消耗,**这样就有点"舍本逐末"了。针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中成为空闲状态。等待下一次任务的执行。
Executors创建线程池
JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。
使用Executors中所提供的静态方法来创建线程池:
- static ExecutorService newCachedThreadPool() 创建一个默认的线程池
- static ExecutorService newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池
提交线程任务:
- Future<?> submit(Runnable task):提交一个Runnable任务,返回一个Future对象,可以通过get方法获得线程运行的结果
- Future<?> submit(Callable task):提交一个Callable任务,返回一个Future对象,可以通过get方法获得线程运行的结果
关闭线程池的方法:
- void shutdown() : 关闭之后,已经提交的任务,执行完毕,后续不让提交任务了
- List shutdownNow() :立即关闭线程池,已经正在运行的任务,运行完毕,在队列中,正在等待运行的任务,放到 list集合当中返回回来
枚举
间接的表示一些固定的值,Java就给我们提供了枚举,是指将变量的值一一列出来,变量的值只限于列举出来的值的范围内;对象的内容和个数已经确定了的类 ,枚举项:就是枚举类当中的一个一个的被确定了的对象。
定义格式:
public enum 枚举名{
枚举项1,枚举项2,枚举项3;
}
枚举的使用场景: 有些情况下,类型中可以创建的对象的个数是固定的,就可以使用枚举类型
枚举类型,源文件.java , 编译后的文件.class
枚举的特点:
- 所有枚举类都是Enum的子类
- 我们可以通过"枚举类名.枚举项名称"去访问指定的枚举项
- 每一个枚举项其实就是该枚举的一个对象, 枚举项写作方式: 枚举对象名称,枚举对象名称,…最后一个对象名称;
- 枚举类的第一行上必须是枚举项,最后一个枚举项后的分号是可以省略的,但是如果枚举类有其他的内容,这个分号就不能省略。建议不要省略
- 枚举也是一个类,也可以去定义成员变量
- 枚举类可以有构造器,但必须是private的,它默认的也是private的.
- 枚举类也可以有抽象方法,但是枚举项必须重写该方法
枚举类型中的常用方法:
- ordinal(): 获取枚举类型中的枚举序数,序数根据定义的枚举项,从0开始,返回值int
- compareTo(E o) : 比较枚举项之间的顺序大小,方法调用枚举项的序数减去参数枚举项的序数
- name() : 将枚举项转换成String类型
- toString() : 将枚举项转换成String类型
- static values() : 将一个枚举类型中的所有枚举项获取到,返回值类型枚举类型的数组
饿汉式与懒汉式对比:
1. 饿汉式: 拿空间(内存)换时间
2. 懒汉式: 拿时间换空间(内存)
综合比较下来, 饿汉式更加优秀
反射
虚拟机加载机制
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
类加载过程:当程序要使用某个类时,如果该类还未被加载到内存中,系统会通过加载,连接,初始化三步来实现对这个类的加载。
加载 :
- 就是指将class文件读入内存,并为之创建一个Class对象.
任何类被使用时系统都会建立一个Class对象new Person(); 使用Person中的成员变量, 方法, 构造(不是手动调用)
连接:
- 验证是否有正确的内部结构,并和其他类协调一致
- 准备负责为类的静态成员分配内存,并设置默认初始化值
- 解析将类的二进制数据中的符号引用替换为直接引用;int i = 10;代码中, 所有使用i的部分, 直接转换10进行使用
初始化 :
主要对类变量进行初始化
a: 类还未被加载, 程序先加载并连接该类
b: 如该类还有直接父类, 则先初始化其直接父类
c: 有初始化语句,按顺序执行
**类的初始化时机:**什么时候创建出该类的字节码对象 Class对象
1.创建类的实例 new Person();
2.类的静态成员使用 Person.name
3.类的静态方法调用 Person.eat()
4.使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
5.初始化某个类的子类: Object类是Person类的父类, 当需要使用Person类时,Object类就需要进到内存中
6.直接使用java.exe命令来运行某个主类
类加载器
类加载器是负责加载类的对象,将class文件加载到内存中,并为之生成对应的java.lang.Class对象。
分类:
- Bootstrap ClassLoader 引导类加载器,通常表示为null
也被称为根类加载器,负责Java核心类的加载,比如System,String等.
- Extension ClassLoader 扩展类加载器
负责JRE的扩展目录中jar包的加载,在JDK中JRE的lib目录下ext目录.
- Application ClassLoader 系统类加载器
负责在JVM启动时加载来自java命令的class文件,以及classpath环境变量所指定的jar包和类路径.
- 自定义类加载器
开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
类加载器之间的关系:
-Bootstrap ClassLoader
-Extension ClassLoader
-Application ClassLoader
双亲委派机制
是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器.每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载.
工作过程:
1)当Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成.
2)当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成.
3)如果Bootstrap ClassLoader加载失败,就会让Extension ClassLoader尝试加载.
4)如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载.
5)如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载.
6)如果均加载失败,就会抛出ClassNotFoundException异常.
ClassLoader类
ClassLoader 叫做类加载器.虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流” 也就是.class字节码文件,这个动作放到java虚拟机外部去实现,以便让应用程序自己决定去如何获取所需要的类,实现这个动作的代模块称之为“类加载器”.把【.class】文件加载到jvm虚拟机当中,转换为一个Class的对象【类的字节码对象 类的类对象】
ClassLoader的方法:
static ClassLoader getSystemClassLoader()
返回用于委派的系统类加载器
ClassLoader getParent()
返回父类加载器进行委派
反射应用
反射是指在运行时去获取一个类的变量和方法信息.然后通过获取到的信息来创建对象,调用方法的一种机制。由于这种动态性,可以极大的增强程序的灵活性,程序不用在编译期就完成确定,在运行期仍然可以扩展。
- 因为要使用实体类, 所以实体类对应的.class字节码文件就会被类加载器从磁盘路径上加载进入到内存中
- 一个类型一旦进入到内存,证明需要使用,证明代码需要运行, 这种状态就是动态的效果
- 类加载器为这个正在运行的.class字节码文件, 创建出一个对应的Class对象, 对象中包含了class文件中所有代码内容(可以包含类中所有成员变量, 构造方法,方法…)
- 使用Class对象, 获取出类型中所有的需要的成员, 这种使用方式称为反射
获取Class类对象的三种方式
- Class类: Class类型的实例表示正在运行的java应用程序的类或者接口。
- Class类的对象: 想获取和操作类中的内容**,首先**要获取类的字节码对象(Class类对象),每一个正在运行的类,都有对应的字节码对象,获取了类的字节码对象,就可以使用这个对象的所有方法,这些方法都定义在Class类型中.
1)类名.class属性
2)对象名.getClass()方法
3)Class.forName(全类名)方法
全类名 : com.ujiuye.demos.Demo01 包名 + 类名
public class Demo01_Class对象获取方式三种 {
public static void main(String[] args) throws Exception {
//reflect: 表示反射的含义
// 1. 使用Person类型: 反射式使用
// 获取Person类型进入内存之后, 对应的.class字节码文件对象, 有三种获取方式
// 1) 任何类型(基本数据类型都包含在内)都有一个属性class, 是系统默认自动添加的
// 返回值是Class类型: 任意在内存中运行的类,接口,枚举,数组,基本数据类型, 都是Class实例化对象
Class c1 = Person.class;
System.out.println(c1);
// 2) Class类中:static forName(带有完整包名的类);
Class c2 = Class.forName("com.ujiuye.reflect.Person");
System.out.println(c2);
// 3) 创建出一个Person类型对象:从父类Object中继承到方法getClass(),
// 获取一个类型对应的字节码文件对象
Person p1 = new Person();
Class c3 = p1.getClass();
System.out.println(c3);
// 4) 一个类型对应的.class字节码文件对象在内存中只有一份
System.out.println(c1 == c2);// true
System.out.println(c2 == c3);// true
}
}
反射获取构造方法并使用
-
Class类获取构造方法对象:
Constructor<?>[] getConstructors()
返回所有public公共构造方法对象的数组
Constructor<?>[] getDeclaredConstructors()
返回所有构造方法对象的数组
Constructor getConstructor(Class<?>… parameterTypes)
返回单个公共构造方法对象
Constructor getDeclaredConstructor(Class<?>…parameterTypes)
返回单个构造方法对象
注意
getConstructor(Class<?>… parameterTypes)
getDeclaredConstructor(Class<?>…parameterTypes)
两方法的参数列表为可变参数,可变参数即参数的个数可以是任意个,0个,1个或者多个均可,任意的数据类型都有对应Class对象, 连基本数据数据类型也不例外 : int.class
Constructor类型:
1)Constructor类表达的含义就是一个类当中的构造方法,一个对象就表达了一个构造方法。
2)构造方法对象应该具有的功能: 获取构造方法各种信息(构造方法修饰符、构造方法名称、构造方法的参数列表、构造方法的注解),最基本的一个功能就是,创建对象。
Constructor用于创建对象的方法:
T newInstance(Object…initargs) 根据指定的构造方法创建对象,参数为所运行构造方法需要的实际参数.
import java.lang.reflect.Constructor;
import java.util.Arrays;
public class Demo02_反射获取构造并执行 {
public static void main(String[] args) throws Exception {
// 1. 先获取Person类型字节码文件对象
Class c1 = Person.class;
// 2. 获取Person类型中所有的公共(public)修饰构造方法
Constructor[] conArr = c1.getConstructors();
System.out.println(Arrays.toString(conArr));
// 3. 获取Person类型中的所有构造
Constructor[] conArrAll = c1.getDeclaredConstructors();
System.out.println(Arrays.toString(conArrAll));
// 4.获取指定的某一个公共修饰构造方法
// Constructor getConstructor(Class<?>... parameterTypes)
Constructor con1 = c1.getConstructor();
System.out.println(con1);
/*Person(String name, int age) {
this.name = name;
this.age = age;
}*/
// 注意: getConstructor只能获取公共修饰构造
/*Constructor con2 = c1.getConstructor(String.class,int.class);
System.out.println(con2);*/
// 5.Constructor getDeclaredConstructor(Class<?>...parameterTypes)
// 获取任意某一个构造方法(构造权限修饰不进行验证)
Constructor con2 = c1.getDeclaredConstructor();
System.out.println(con2);
// 私有权限修饰构造
Constructor con3 = c1.getDeclaredConstructor(String.class,int.class,String.class,String.class);
System.out.println(con3);
// 6. 运行构造方法: 创建出指定类型对象, 同时利用有参构造为这个对象成员变量进行赋值
// Constructor: 构造器类型中有一个方法功能, newInstance(Object...initargs)
Person p1 = (Person)con2.newInstance();
System.out.println(p1.getName());// null
System.out.println(p1.getAge());// 0
// 获取出默认修饰的构造方法并运行
Constructor conDefault = c1.getDeclaredConstructor(String.class,int.class);
Person p2 = (Person)conDefault.newInstance("张三",20);
System.out.println(p2.getName());// 张三
System.out.println(p2.getAge());// 20
// con3先不要运行,使用private修饰符, 虽然将私有构造获取到了,运行时,还是会有权限修饰限制
/* Person p3 = (Person)con3.newInstance("lisi",19,"nan","北京");
System.out.println(p3.getName());
System.out.println(p3.getAge());*/
// 7. Class类中有便捷的创建对象方式:
// 要求: 类型中带有public公共修饰的空参数构造, 直接调用Class类型中的newInstance即可
// Class c1 = Person.class;
Person p4 = (Person)c1.newInstance();
System.out.println(p4.getName());
}
}
反射获取成员变量 并 使用
Class类获取成员变量对象:
- Field[] getFields():返回所有公共成员变量对象的数组
- Field[] getDeclaredFields():返回所有成员变量对象的数组
- Field getField(String name):返回单个公共成员变量对象,参数name表示成员变量的名字
- Field getDeclaredField(String name):返回单个成员变量对象,参数name表示成员变量的名字
- Field类型: 表示一个成员变量类型,每个对象都是一个具体的成员变量
作用: 获取成员变量的各种信息(修饰符、注解、名称),做各种数据类型的转换.
- Field类用于给成员变量赋值的方法:
set(Object obj, Object value): 用于给obj对象的,指定成员变量,赋value值
- Field类获取成员变量值的方法:
get(Object obj): 用于获取obj对象的指定成员变量值
import java.lang.reflect.Field;
import java.util.Arrays;
public class Demo03_反射获取成员变量并使用 {
public static void main(String[] args) throws Exception{
// 1. 获取字节码文件对象
Class c1 = Class.forName("com.ujiuye.reflect.Person");
// 2. 获取出Person类中所有公共修饰成员变量
Field[] fArr = c1.getFields();
System.out.println(Arrays.toString(fArr));
// 3. 获取出Person类中所有的成员变量
Field[] fArrAll =c1.getDeclaredFields();
System.out.println(Arrays.toString(fArrAll));
// 4. Field getField(String name)
// 返回单个公共成员变量对象,参数name表示成员变量的名字
Field add = c1.getField("address");
System.out.println(add);
// 5. Field getDeclaredField(String name)
// 返回单个成员变量对象,参数name表示成员变量的名字
Field age = c1.getDeclaredField("age");
System.out.println(age);
// 6. Field类中的: set(Object obj, Object value): 用于给obj对象的,指定成员变量,赋value值
// 为p1对象中的address成员变量地址进行赋值
Person p1 = (Person)c1.newInstance();
add.set(p1,"香港");
System.out.println(p1.address);
// 7. Field类中: get(Object obj): 用于获取obj对象的指定成员变量值
// 获取p1对象中的address成员变量值
String address = (String)add.get(p1);
System.out.println(address);
Person pp1 = new Person();
Person pp2 = new Person();
pp1.address = "xinjiapo";
System.out.println(pp1.address);
}
}
获取类中的成员方法并执行
Class类获取成员方法对象:
-
Method[] getMethods():返回所有公共成员方法对象的数组,包括继承的
-
Method[] getDeclaredMethods():返回所有成员方法对象的数组,不包括继承的
-
Method getMethod(String methodName, Class<?>…parameterTypes):返回单个公共成员方法对象
-
Method getDeclaredMethod(String methodName, Class<?>…parameterTypes):返回单个成员方法对象
Method类型:
(1) 表示成员方法的类型,该类型的每个对象,都是一个具体的成员方法
(2) 成员方法对象具有的功能: 获取成员方法信息,运行方法.
Method类用于执行方法的功能:invoke(Object obj, Object…values):调用obj对象的成员方法,参数是values是运行方法的实际参数,返回值Object类型是方法运行的返回值结果。
import java.lang.reflect.Method;
import java.util.Arrays;
public class Demo04_反射获取方法并运行 {
public static void main(String[] args) throws Exception{
Class c1 = Person.class;
// 1. 获取出Person类中的所有公共修饰方法(包括从父类继承而来的)
Method[] mArr = c1.getMethods();
System.out.println(Arrays.toString(mArr));
// 2. 获取出Person类中的所有方法(不包括继承到的)
Method[] mArrAll =c1.getDeclaredMethods();
System.out.println(Arrays.toString(mArrAll));
// 3. Method getMethod(String methodName, Class<?>...parameterTypes)
// 返回单个公共成员方法对象
Method setAge = c1.getMethod("setAge",int.class);
System.out.println(setAge);
// 4. Method getDeclaredMethod(String methodName, Class<?>...parameterTypes)
// 返回单个成员方法对象
Method eat1 = c1.getDeclaredMethod("eat",String.class,int.class);
System.out.println(eat1);
// 5. Method类用于执行方法的功能:
// invoke(Object obj, Object...values):调用obj对象的成员方法,
// 参数是values是运行方法的实际参数,返回值Object类型是方法运行的返回值结果.
Person p1 = (Person)c1.newInstance();
setAge.invoke(p1,28);
System.out.println(p1.getAge());
String obj = (String)eat1.invoke(p1,"米饭",28);
System.out.println(obj);
}
}
暴力反射
通过Class类中:
getDeclaredXXX方法: 可以获取类中所有声明的成员(属性、方法、构造),私有的成员也可以获取到.但是私有成员进行访问使用时,会因为权限问题导致失败,因此就需要暴力反射解决访问私有的问题。
修改该对象的访问权限:
AccessibleObject类是Field,Method和Constructor对象的基类. 它提供了将反射对象标记为在使用它时抑制默认Java语言访问控制检查的功能.
setAccessible(boolean flag): true的值表示反射对象应该在使用时抑制Java语言访问检查,false的值表示反映的对象应该强制执行Java语言访问检查.
一旦设定当前对象可以访问,私有的成员也可以被访问,被修改。
泛型擦除
泛型擦除: 使用泛型,泛型好处限制数据的类型,从而提高代码的安全性和简洁性,但是泛型只是存在于编译时期, 代码实际运行时,.class字节码文件中不会出现泛型。
JDK新特性
接口的新特性
jdk8之前接口是规则的集合体,方法只有抽象方法。
jdk8版本开始不光有抽象方法同时增加了实体方法。
jdk8:default默认方法, static静态方法
jdk9: private私有方法
进行接口功能维护, 但是因为接口中全是抽象方法,改动接口时, **涉及到向接口内添加抽象方法; 接口的改动, 会影响其所有的实现类,强制实现类重写抽象方法;**即可以调整接口, 又对实现类影响降到最低, 于是就在JDK8版本中, 引入了默认方法和静态方法, 让这些方法拥有方法体, 不再让实现类强制重写,从而提高接口的可维护性。
默认方法:
给实现类直接继承使用, 实现类可以选择直接继承, 可以选择重写
- 被关键字 default 修饰的方法就是默认方法,是在jdk8版本才出现的方法,独属于接口所有。
- 出现的原因:在jdk8版本的时候,需要对一个接口下面的所有的实现类的功能做一个增强,就需要在接口当中去添加方法,如果接口中添加的是抽象方法,下面的实现类就需要强制去重写这些抽象方法,jdk希望在接口当中添加方法,直接就让下面的实现类去使用,不用再次的进行重写,所以添加了使用default做修饰的默认方法,默认方法是可以不被重写的,因为他有方法体。
- 使用规则
- 加上default的,实现类可以不重写,直接调用
- 特殊情况1:实现类实现了两个接口,如果有相同的默认方法声明,则强制要求在实现类中,重写这个方法,以指定确定的实现内容
- 特殊情况2:在特殊情况1中,如果在实现类重写这个方法的过程中,希望指定其中某一个接口的默认实现,则可以调用”父接口名.super.默认方法名称(实际参数)”
- 特殊情况3:实现类实现了继承了一个父类,并且实现了一个接口,并且在父类中和接口中,有相同的方法声明,则“类优先”。即使继承的是一个抽象类,也是使用父类的实现(即强制重写)。
- 产生的影响
- 接口中如果也可以定义非抽象方法,那么接口和抽象类的差别就越来越小
- java中一个类只能继承一个抽象类,但是可以同时实现多个接口,所以有了默认方法,就会让大量的抽象类变成接口,即弱化了抽象类
静态方法
1、接口的静态方法可以定义方法体内容
2、static不能和abstract共存
3、外界只能通过接口名称.静态方法来访问接口静态方法
4、实现类中不会继承接口中的静态方法,原因:如果一个类同时实现了两个接口,具有相同的静态方法名,继承之后不知道应该以哪个为准, 而静态方法又不能重写,因此矛盾无法解决 【 可以使用不能重写】
私有方法
私有方法是jdk9版本增加的一个实体方法,主要是用来进一步封装代码,提升相关代码安全性的手段。私有化之后方法不能被实现类直接调用使用或重写修改,只能提供给接口的静态方法和默认方法使用。
- 普通私有方法:只能提供给默认方法调用使用
- 静态私有方法:默认方法和静态方法都可以调用
私有存在目的就是为了安全性, 接口中提供的私有方法, 就是为了本接口中的默认和静态方法调用
Lambda表达式
Lambda表达式是java对数学函数表达式的一种体现形式,本质是一个值,在java中主要是体现在对特殊的匿名内部类对象的一种表示,代表匿名内部类的对象。
使用前提:函数式接口:只有一个抽象方法的接口(可以有其他非抽象方法)
可以在接口声明之上, 使用注解: @FunctionalInterface标识验证这个函数式接口
优点:对匿名内部类对象的格式简化,大幅提升开发效率
注: Lambda表达式代表的匿名内部类的对象,在编译的时候不需要生成对应类的字节码文件,而匿名内部类在编译的时候需要编译生成对应的字节码文件,所以Lambda表达式可以提高效率。
格式:(参数列表) -> {方法体}
详细说明:
- (参数列表): 表示要实现的接口中,抽象方法的参数
- -> : 箭头运算符,或者称为Lambda运算符,用于分隔前后两部分
- {方法体}: 也称为Lambda体,表示重写抽象方法的具体实现
@FunctionalInterface
public interface MyInter {
void fun();
}
class TestMyInterface{
public static void main(String[] args) {
// 1. 使用匿名内部类实现MyInter接口
MyInter my1 = new MyInter(){
@Override
public void fun() {
System.out.println("我是匿名内部类对象实现的fun方法");
}
};
my1.fun();
// 2. 使用Lambda表达式实现MyInter接口
/*
Lambda表达式语法结构: (参数列表) -> {方法体}
1) (参数列表): 唯一需要重写的抽象方法对应的参数列表
2) -> : Lambda运算符(箭头运算符), 连接需要实现的方法参数和方法体
3) {方法体}: 唯一需要重写的抽象方法对应的实现过程
注意: Lambda表达式使用前提, 只能作为函数式接口的实现类对象
*/
MyInter my = ()->{
System.out.println("Lambda表达式实现的fun方法");
};
my.fun();
}
}
特殊情况
-
有且只有一个参数,可以省略小括号
x -> {int result = x * x; System.out.println(x + “的平方为:” + result);}
-
Lambda体只有一句,且没有返回值,可以省略大括号
x -> System.out.println(x * x);
-
Lambda体只有一句,且有返回值,则return和大括号可以一起省略
(x, y) -> {return x + y;} 等价于 (x, y) -> x + y
注意:要么一起省略,要么都不省略
函数式接口
如果在接口中,只有一个抽象方法,那么这个接口就是函数式接口,比如:jdk中熟悉的函数式接口 :Runnable Callable。
使用注解来检查当前接口是否是一个函数式接口
@FunctionalInterface
如果不是函数式接口,则编译报错
-
函数:想表达的是一个方法的内容,由于方法不在任何类中,所以称为函数
-
函数式接口:其实想表达的就是一个抽象函数的声明
-
作用:使用函数式接口表达函数的声明;使用函数式接口的实现类对象表达函数的实现
-
使用原因:
Java中不支持将函数作为一个数据,也就不能将这个函数进行各种传递,也就不能作为对象的成员变量存在
只能在方法外加一层接口的声明,将来可以传递方法所在接口的实现类对象,来间接的传递方法实现内容
常用的内置接口
Java8中提供了一些常用的函数式接口,在使用类似功能的时候,不需要额外定义接口,直接使用jdk中提供的即可
-
Consumer:消费型接口
**void accept(T t): 需要传进进来参数,进行消费,没有返回值。【有进无出】**
定义一个方法,这个方法需要传递进来数据,还需要传递进来处理这个数据的方式,并且还不需要返回值类型,就可以使用Consumer接口来当做数据处理方式的传递。
有了Consumer接口接口以后,不仅仅可以传递数据,还可以传递消费处理数据的方式。
import java.util.function.Consumer;
public class TestConsumer {
public static void main(String[] args) {
// 1)用户1: 花费money元,购买化妆品
Consumer<Double> con1 = m -> System.out.println("花了" + m + "元买了化妆品");
testConsumer(500,con1);
// 2)用户2: 花费money元, 买了球鞋
Consumer<Double> con2 = m -> System.out.println("花了" + m + "元买了球鞋");
testConsumer(499,con2);
// 3)用户3: 花费money元, 去旅游
Consumer<Double> con3 = x -> {
if(x > 5000){
System.out.println("攒钱到" + x + "元,再去旅游");
}else{
System.out.println("愉快的去了西藏旅游,花费" + x + "元");
}
};
testConsumer(3000,con3);
testConsumer(6000,con3);
}
/*
案例: 定义出一个double类型money, 每个用户对于money的消费方式都不同
1)用户1: 花费money元,购买化妆品
2)用户2: 花费money元, 买了球鞋
3)用户3: 花费money元, 去旅游
4)....
分析:
1. 第一个参数: 消费的金额double money
2. 第二个参数: 对于money消费方式, 可以提供Consumer<Double>, 相当于
将接口的唯一抽象方法 void accept(double money)
*/
public static void testConsumer(double money, Consumer<Double> con){
con.accept(money);
}
}
-
Supplier:供给型接口
**T get(): 不需要传递参数,返回创建一个数据的接口 【无进有出】**
当一个方法需要去生产一些数据,并且生产数据的方式不确定,就可以在方法的形参列表位置传递一个 Supplier接口,将来让调用该方法的调用者把如何生产数据的方式传递进来。有了供给型接口之后,不仅仅可以传递生产数据的数量等信息,也可以传递生产数据的方式。
import java.util.ArrayList;
import java.util.Random;
import java.util.function.Supplier;
public class TestSupplier {
public static void main(String[] args) {
// 1)客户1: 5个数据, 都是30-80之间随机数
Supplier<Integer> sup1 = ()->new Random().nextInt(51) + 30;
/*Random ran = new Random();
return ran.nextInt(51) + 30;*/
ArrayList<Integer> list = testSupplier(5,sup1);
System.out.println(list);
// 2)客户2: 8个数据, 1-100之间随机偶数
Supplier<Integer> sup2 = () -> {
Random ran = new Random();
int number = ran.nextInt(100)+1;
while(number % 2 != 0){// 奇数,继续生成
number = ran.nextInt(100)+1;
}
return number;
};
System.out.println(testSupplier(8,sup2));
}
/*案例: 定义出一个方法功能, 能给客户返回一个ArrayList<Integer>容器,
容器中存储的数据个数以及存储数据的规律, 由客户决定
1)客户1: 5个数据, 都是30-80之间随机数
2)客户2: 8个数据, 1-100之间随机偶数
3)...
分析:
1. 第一个参数: 提供容器中存储的数据个数
2. 第二个参数: 计算出需要的存储在集合中的数据的规律, 将Supplier<Integer>,
实际传递的是Supplier中唯一抽象方法 Integer get()
*/
public static ArrayList<Integer> testSupplier(int number, Supplier<Integer> sup){
ArrayList<Integer> list = new ArrayList<>();
for(int i = 1; i <= number; i++){
list.add(sup.get());
}
return list;
}
}
-
Function<T, R>:函数型接口
**R apply(T t): 需要传递一个参数,把传递进来的参数进行一些操作,然后再返回一个参数【有进有出】**
如果需要定义一个函数,接收一个数据,将数据进行处理,完成之后,还能返回一个结果,就可以使用函数型接口,以前我们只能传递处理好之后的数据,或者将原始数据传入方法,现在可以传入处理方式。
Function andThen(Function f):在调用者处理方式之后,再进行参数的处理方式调用。
import java.util.function.Function;
public class TestFunction {
public static void main(String[] args) {
// 1)客户1: y为x的2倍
Function<Integer,Integer> fun1 = x->2*x;
System.out.println(testFunction(10,fun1));// 20
// 2)客户2: y与x相等
System.out.println(testFunction(5,x->x));// 5
// 3)客户3: y为x的平方
System.out.println(testFunction(6,x->x*x));// 36
// 测试andThen方法使用
Function<String,Integer> fun2 = x -> Integer.parseInt(x);
Function<Integer,Integer> fun3 = x -> 2*x + 1;
System.out.println(testFunction2("17",fun2,fun3));// 35
}
/*案例: 定义方法功能, 根据整数x, 计算出对应整数y, x数据由客户给出, y数据计算方式根据客户要求决定
1)客户1: y为x的2倍
2)客户2: y与x相等
3)客户3: y为x的平方
4)...
分析
1. 第一个参数表示用户提供的整数x
2. 第二个参数: 需要通过x计算出y, Function<Integer,Integer>, 将唯一抽象方法apply作为计算思想
Integer apply(Integer)
*/
public static int testFunction(int x, Function<Integer,Integer> fun){
return fun.apply(x);
}
/*案例: 定义方法功能, 根据String类型x, 计算出对应整数y, x数据由客户给出, y数据计算方式根据客户要求决定
1)客户1: y为x的2倍
2)客户2: y与x相等
3)客户3: y为x的平方
4)...
分析
1. 第一个参数表示用户提供的String类型x
2. 第二个参数: Function<String,Integer> fun1, 以字符串x,转换成Integer类型x
3. 第三个参数: Function<Integer,Integer> fun2, 将fun1转换后的整数x,计算出整数y
*/
public static int testFunction2(String x, Function<String,Integer> fun1,
Function<Integer,Integer> fun2){
// fun1调用后面的apply方法, 传递的参数 String类型的x, 得出Integer
// andThen方法, 将fun1的applay结果作为参数fun2中applay方法的实参
// return fun1.andThen(fun2).apply(x);
return fun2.apply(fun1.apply(x));
}
}
-
Predicate:断言型接口
**boolean test(T t): 需要参数,返回一个boolean类型的值【有进有出,出来的结果一定是boolean类型】**
如果需要定义一个函数,接收一个数据,判断数据是否合法,返回一个boolean结果,就可以使用断言型接口,以前我们只能传递过滤好的数据,而现在既可以传递原始数据,也可以传递过滤的条件
Predicate and(Predicate pre):在调用者条件判断之后,再由参数条件判断,返回两个条件都满足的判断对象
Predicate or(Predicate pre):返回两个条件任意一个满足的判断对象
Predicate negate():取判断结果的反向结果
import java.util.ArrayList;
import java.util.function.Predicate;
public class TestPredicate {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(15);
list.add(88);
list.add(100);
list.add(101);
list.add(166);
list.add(3);
list.add(18);
// 1)客户1: 要求容器中所有数, 都能被2整除
Predicate<Integer> pre = u -> u % 2 == 0;
System.out.println(testPredicate(list,pre));
// 2)客户2: 要求所有的数据都不大于100
Predicate<Integer> pre1 = t -> t < 100;
System.out.println(testPredicate(list,pre1));
// 3)客户3: 要求所有数据都小于100,并且是奇数
Predicate<Integer> pre2 = x-> x<100 && x%2!=0;
System.out.println(testPredicate(list,pre2));
// 3)客户3: 要求所有数据都小于100,并且是奇数
// pre1对应小于100验证规则; pre取反规则
System.out.println(testPredicate(list,pre1.and(pre.negate())));
// 4)客户4: 要求所有数据或者小于100或者是偶数
System.out.println(testPredicate(list,pre1.or(pre)));
}
/*案例: 定义出一个方法, 需要客户提供一个ArrayList<Integer>容器,
根据客户的需求, 将容器中符合条件数据筛选出来, 将筛选出的数据放置在新容器中返回给客户
1)客户1: 要求容器中所有数, 都能被2整除
2)客户2: 要求所有的数据都不大于100
3)客户3: 要求所有数据都小于100,并且是奇数
4)客户4: 要求所有数据或者小于100或者是偶数
5)...
分析:
1. 第一个参数: 客户提供的ArrayList初始容器(待筛选容器)
2. 第二个参数: 为了验证ArrayList中的每一个整数是否符合规则, 规则不尽相同,
因此提供一个接口 Predicate<Integer>, 实际上就是为了提供抽象方法 boolean test(Integer)
*/
public static ArrayList<Integer> testPredicate(ArrayList<Integer> list,
Predicate<Integer> pre){
ArrayList<Integer> l = new ArrayList<>();
if(list != null){
for(Integer i : list){
if(pre.test(i)){
l.add(i);
}
}
}
return l;
}}
方法的引用
写一个函数式接口时,方法的实现(lambda体),已经被某个其他的对象实现了,就不需要在Lambda体中,再次实现一遍,而可以直接使用那个已经定义好的方法。
函数式接口 名称 = 对象名 :: 方法名称;
函数式接口 名称 = 类名 :: 静态方法名;
把已经实现的方法,作为一个数据,作为实现类对象,赋值给某个函数式接口的引用
可以把这个引用当做方法的返回值,也可以作为方法的实际参数进行传递
@FunctionalInterface
public interface FunctionInter {
public abstract void print(int n);
}
class InterMethod1{
public void ok(int x){
System.out.println(x*2);
}
}
class InterMethod2{
public static void print(int x){
System.out.println(x+"abc");
}
}
class TestInter{
public static void main(String[] args) {
// 使用lambda表达式实现FunctionInter接口
FunctionInter fi = x-> System.out.println(x*2);
fi.print(5);
// 方法引用: lambda实现的方法过程与某个类中已经定义好的方法内容一致,lambda表达式可以不写, // 通过方法引用的方式, 借用指定类的方法作为lambda表达式
// 1) 对象名 :: 方法名; (方法非静态)
// 2) 类名 :: 方法名; (方法静态)
FunctionInter fi2 = new InterMethod1() :: ok;
fi2.print(10);
FunctionInter fi3 = InterMethod2 :: print;
fi3.print(19);
}
}