1).跨平台性:
2).运行原理:
2.1)基本类型的类型转换:
口诀:
小到大,直接转 大到小,强制转 浮变整,
小数没
低 ----------> 高
byte,short,char→ int→ long→float
→double
1.变量:
我们通过三个元素来描述变量:变量类型 变量名以及变量值。
- 变量名必须是一个有效的标识符
- 变量名不可以使用java关键字
- 变量名不能重复
1.2)常量:
在程序运行过程中,值一直不会改变的量成为常量
2.2)局部变量:
位置:定义在方法里或者局部代码块中
注意:必须手动初始化来分配内存.如:int i = 5;或者int i; i = 5;
作用域:也就是方法里或者局部代码块中,方法运行完内存就释放了
2.3)成员变量:
位置:定义在类里方法外
注意:不用初始化,也会自动被初始化成默认值
作用域:整个类中,类消失了,变量才会释放
3.1)自增自减运算符
自增(++):将变量的值加1
分前缀式(如++a)和后缀式(如a++)。前缀式是先加1再使用;后缀式是先使用再加1。
自减(–):将变量的值减1
分前缀式(如–a)和后缀式(如a–)。前缀式是先减1再使用;后缀式是先使用再减1。
3.2)逻辑运算符
通常,我们用0表示false,用1表示true
与:表示并且的关系
&单与: 1 & 2 ,结果想要是true,要求1和2都必须是true
&&双与(短路与):1 && 2 ,当1是false时,2会被短路,提高程序的效率
或:表示或者的关系
|单或: 1 | 2,结果想要是true,要求1和2只要有一个为true就可以
||双或(短路或):1 || 2,当1是true时,2会被短路,提高程序效率
==比较运算符,用来比较这个运算符左右两边表达式是否相等,结果是布尔类型的值
=赋值, 它们都是运算符
4.1)方法调用顺序
定义:
在同一类中,存在方法名相同,但参数列表不同的方法,
如果在同一类中,同名方法的参数个数不同,一定构成重载.
如果在同一类中,同名方法的参数个数相同,
查看对应位置上的参数类型,不是参数名
演示:
(int a,String s)与(int s,String a)--不构成重载.
(int a,String b)与(String b int a)--重载
1.不需要看参数名字.
2.如何判断是否重载:
看它是否在同一类中,其次它们的方法名相同,它们的参数,个数不同,所以构成重载.
5.1)扫描器:
演示:
System.out.println("请输入你有计算的第一个数据:"); int(类型) a = new Scanner(System.in).nextInt();(类型)
(下标)就像每栋楼的楼门号一样
6.1)创建数组过程分析:
程序创建数组 int[] a = new int[5]; 时发生了什么?
在内存中开辟连续的空间,用来存放数据,长度是5
给数组完成初始化过程,给每个元素赋予默认值,int类型默认值是0
数组完成初始化会分配一个唯一的地址值
把唯一的地址值交给引用类型的变量a去保存
6.2)两种创建数组:
静态创建--已经知道了数组中的每个位置上的具体元素
char[] c2= {'h','2'};
动态创建--知道了数组的长度,后期再动态的存入数据
char[] c2 = new char[5]
注意:
1. 数组名是个引用类型的变量,它保存着的是数组的地址,不是数组中的数据.
2. a[i] = new Random().nextInt(100)+1;//取值范围前后都+1-->[1,101) 左闭右开.
3. 除了char类型以外的所有数组想要查看数组中的具体元素
需要使用数组的工具类Arrays的toString(数组名)方法 (查看数组中具体元素)
注意:Arrays使用时需要导包
String[] s = {"a","b","c"};
System.out.println(Arrays.toString(s));
6.3)创建数组方法
那如果通过编程,我们该怎么实现呢?
如下图:我们可以通过嵌套for循环来实现:
外层循环来控制比较的轮数:
最大轮数=个数-1 内层循环来控制每轮比较的次数
在比较过程中,如果顺序不对,就互换元素的位置
package cn.tedu.array;
import java.util.Arrays;
/**本类用来完成冒泡排序*/
public class TestBubbleSort {
public static void main(String[] args) {
//1.创建一个无序的数组
int[] a = {27,96,73,25,21};
//2.调用method()完成排序
int[] newA = method(a);
System.out.println("排序完毕:"+Arrays.toString(newA));
}
public static int[] method(int[] a) {
//1.外层循环,控制比较的轮数,假设有n个数,最多比较n-1次
//开始值:1 结束值:<= a.length-1 变化:++
//控制的是循环执行的次数,比如5个数,最多比较4轮,<= a.length-1,最多取到4,也就是[1,4]4次
for(int i = 1 ; i <= a.length-1 ; i++) {
System.out.println("第"+i+"轮:");
//2.内层循环:相邻比较+互换位置
for(int j=0; j < a.length-i ; j++) {
//相邻比较,a[j]代表的就是前一个元素,a[j+1]代表的就是后一个元素
if(a[j] > a[j+1]) {
//交换数据
int t = a[j];
a[j] = a[j+1];
a[j+1] = t;
//System.out.println("第"+(j+1)+"次比较交换后:"+Arrays.toString(a));
}
}
System.out.println("第"+i+"轮的结果:"+Arrays.toString(a));
}
return a;//把排序好的数组a返回
}
}
优化1:
前面几轮排序产生的最大值不需要参与后几轮的比较,执行过几轮就会产生几个值不需要参与比较 , i 轮产生 i 个值,所以需要 - i
优化2:
我们要设置一个量,这个量用来检测在当前这一轮的相互比较中究竟有没有发生元素的互换位置,如果发生了互换,说明顺序还没排好,flag就改成true,进行下一轮比较,但是如果在当前轮,所有元素都进行了相互比较,并没有互换位置,这就说明顺序已经排好序了,无需下一轮比较,直接return结束方法即可
1.1)单分支结构:
if(判断条件){
代码
}
1.2)多分支机构:
if(判断条件){
满足判断条件,执行代码1
}else{
不满足判断条件,执行代码2
}
1.3)嵌套分支机构:
if(判断条件){
满足判断条件,执行代码1
}else if(判断条件2){
不满足判断条件,执行代码2
}else if(判断条件3){
不满足判断条件,执行代码3
}else{
以上条件都不满足,执行代码4
switch 语句中的变量类型可以是: byte、short、int 、char、String(jdk1.5以后支持)
如果在default之前的case有break,则default不会执行
switch(expression){
case value : syso(1)//语句 break;//可选
case value : syso(2)//语句 break;//可选
case value : syso(3)//语句 break;//可选
case value : syso(4)//语句 break;//可选
//可以有任意数量的 case语句
default : syso(0) //语句//可选//语句
}
1.形式(先判断,再执行)
while(执行条件){
循环体;
}
2.形式(先执行,再判断,循环体代码保证最少执行一次)
do{
循环体;
}while(执行条件);
死循环一定要写程序出口
break
for( 开始条件 ; 循环条件 ; 更改条件){
循环体;
}
for 1 2 3 4 5 6 7 8 9 10
从那开始:1
从那结束:10
如何变化:+1 ++
for( 开始条件 ; 循环条件 ; 更改条件){//外层循环
for( 开始条件 ; 循环条件 ; 更改条件){//内层循环
循环体;
}
}
外层循环,控制行数 内层循环,控制列数
- for:知道循环次数
- while/do while:当循环次数不确定时
- while:先判断,不符合规则,不执行代码
- do while:代码最少被执行一次,再去判断,符合规则,再次执行代码
- 循环之间都可以互相替代,但是一般最好选择合适的循环结构来完成代码哦
switch里return和break区别:
break直接退出switch语句
return退出该函数,switch语句块后面的语句也不执行了
return:返回方法返回值,不管你在那个位置,那层循环,遇到break方法直接结束
break:如果是单层循环直接跳出循环,如果是嵌套循环它会跳出内层循环不会跳出外层循环.
continue:是结束本轮后半部分代码,执行下一轮循环,不影响下一轮循环
continue:
注意:不管加不加continue,都可以在猜不对的情况下继续输入,只不过在加了continue后,效率更高,只要输入数据不是正确的,就无需执行循环后半部分代码,直接进行下一轮即可
提高效率,加了continue就不执行循环后半部分代码,直接进入下一轮代码,他跳过的是本轮循环内容,不影响下一轮循环.
continue: 跳出本轮循环,继续下一轮循环:
本轮循环体中的语句不会继续执行,但是会继续执行下轮循环,循环体外也会执行
continue和break之后都不允许写代码,都是不可到达的代码
报错:不可到达的代码
Unreachable code
注意:如果是嵌套for循环,在内层循环遇到break,只会跳出当前这一层循环
重复循环用for循环 判断用分支结
封装: 把相关的数据封装成一个“类”组件
继承: 是子类自动共享父类属性和方法,这是类之间的一种关系
多态: 增强软件的灵活性和重用性
Java语言最基本单位就是类,类似于类型。
类是一类事物的抽象。
可以理解为模板或者设计图纸。
每个对象具有三个特点:对象的状态,对象的行为和对象的标识。
对象的状态用来描述对象的基本特征。
对象的行为用来描述对象的功能。
对象的标识是指对象在内存中都有一个唯一的地址值用来和其他对象区分开来。
类是一类事物的抽象,对象是具体的实现。
计算机语言来怎么描述现实世界中的事物的? 属性 + 行为
那怎么通过java语言来描述呢?
我们可以通过类来描述一类事物,用成员变量描述事物的属性,用方法描述事物的行为.
类中提供的 get/set 设置和获取值
创建类:
类-一类事物的抽象-模板
1.创建类
2.创建类的属性(成员变量)
3.封装类的属性(成员变量)
4.提供get与set方法
5.提供一些本类的功能
创建对象:
创建对象
通过对象调用方法
1)可以调用set方法设置私有属性的值
2)可以调用get方法获取私有属性的值
3)可以调用普通的方法执行对应的功能
当一个类中有两个同名的变量,一个成员变量,一个局部变量
想使用本类的成员变量时,可以用this指定一下this代表的是"本类",由于成员变量属于类资源,所以可以被指定.
我们可以理解成:类名 this = new 类名
但注意仅仅是理解,不能这么写!!!!!
规定:一定注意this关键字必须在构造方法中第一行
this.name = name;
this代表本类对象的一个引用对象
this还可以在构造方法间相互调用
但请一定注意:是单向的,不是双向来回调用,会死循环
为了外部想用我本类的属性的时候按照我的方法来去访问这才是封装的意义.
如何封装?封装后的资源如何访问?
我们可以使用private关键字来封装成员变量与方法
如何访问私有资源?
关于成员变量:
setXxx – 对外提供公共的设置值方式
getXxx – 对外提供公共的获取值方式
关于成员方法:
把私有方法放在公共方法里供外界调用即可
方法名加参数列表
创建对象时先看父类构造方法
Java只支持单继承,关键字:extends
继承可以传递(一脉相传)
父类的私有资源,子类不可见(不可用)
是一种is a的强耦合一个子类只能有一个父类,但是一个父类可以有多个子类
2.2)特点:
- 使用extends关键字来表示继承关系
- 相当于子类把父类的功能复制了一份
- Java只支持单继承
- 继承可以传递(爷爷/儿子/孙子这样的关系)
- 不能继承父类的私有成员
- 继承多用于功能的修改,子类可以在拥有父类功能的同时,进行功能拓展
- 像是is a的关系
概念:可以通过这个关键字使用父类的内容:
Super代表的是父类的一个引用对象
注意:在构造方法里,出现的调用位置必须是第一行
在子类中使用父类的xxx资源,需要使用super.进行指定, super是表示父类的一个对象引用 可以理解成 父类 super = new 父类();
继承是面向对象最显著的一个特征
继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并扩展新的能力.
Java继承是会用已存在的类的定义作为基础建立新类的技术
新类的定义可以增加新的数据或者新的功能,也可以使用父类的功能,但不能选择性的继承父类(超类/基类)
这种继承使得复用以前的代码非常容易,能够大大的缩短开发的周期,降低开发费用.
创建一个抽象父类的构造方法,在创建子类无参构造,(无参构造默认存在),隐藏着super(),先访问父类的构造方法,再执行自己的, 如果父类的处传参,默认的无参构造会被覆盖,子类super()就报错了,在main方法中,子类 p = new 子类 输出的是父类在是子类,因为super先去调用父类无参构造在执行自己的无参构造,在多态中也是一样的.
一切都是为了孩子.(子类)
多态是面向对象程序设计(OOP)的一个重要特征,指同一个实体同时具有多种形式,即同一个对象,在不同时刻,代表的对象不一样,指的是对象的多种形态。
可以把不同的子类对象都当作父类来看,进而屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,统一调用标准
注意:
静态资源属于类资源,不存在重写的现象,只是两个类中有同样声明的方法而已,不属于重写
特点:
多态的前提1:是继承
多态的前提2:要有方法的重写
父类引用指向子类对象,如:Animal a = new Cat();
多态中,编译看左边,运行看右边
演示:
Animal a = new Cat();
new Cat();
创建了一个子类对象用父类引用类型变量来接,
Animal a = new Cat();
父类型的引用类型变量 a 保存着子类对象Cat()地址值
多态的出现是为了统一调用标准,向父类看齐, 父类提供的功能才能用,子类特有的功能用不了
口诀:
1.父类引用 指向子类对象
2.编译(保存)看左边, 运行(测试)看右边
好处:
- 多态可以让我们不用关心某个对象到底具体是什么类型,就可以使用该对象的某些方法
- 提高了程序的可扩展性和可维护性
使用:
- 成员变量: 使用的是父类的
- 成员方法: 由于存在重写现象,所以使用的是子类的
- 静态成员: 随着类的加载而加载,谁调用就返回谁的
多态总结:
多态是什么?
同一种事物多种表现形式,可以把多种子类都当做父类来看,统一标准
2.实现多态的前提是?
继承+重写
3.多态的特点?
1) 父类引用指向子类对象
2) 编译看左边,运行看右边
4.多态的使用?
1) 成员变量 : 父类
2) 成员方法 : 父类的声明 & 子类的实现
3) 静态方法 : 如果父子类有同名静态方法,使用的是父类的
静态资源可以继承,属于类资源,静态不可以重写
异常类型 异常信息 报错行号
继承结构
异常的处理方式
捕获--自己解决try-catch
抛出--让别人去解决throws
,谁调用抛出异常的方法,谁就需要解决这个问题(捕获处理/继续向上抛出)
不要把异常抛给main(),因为没人解决了
在JAVA中,继承是一个重要的特征,通过extends关键字,子类可以复用父类的功能,如果父类不能满足当前子类的需求,则子类可以重写父类中的方法来加以扩展。
在应用中就存在着两种转型方式,分别是:向上转型和向下转型。
比如:父类Parent,子类Child
向上转型:父类的引用指向子类对象Parent p=new Child();
说明:向上转型时,子类对象当成父类对象,只能调用父类的功能,如果子类重写了父类的方法就根据这个引用指向调用子类重写方法。
向下转型(较少):子类的引用的指向子类对象,过程中必须要采取到强制转型。
Parent p = new Child();//向上转型,此时,p是Parent类型
Child c = (Child)p;//此时,把Parent类型的p转成小类型Child
//其实,相当于创建了一个子类对象一样,可以用父类的,也可以用自己的
说明:向下转型时,是为了方便使用子类的特殊方法,也就是说当子类方法做了功能拓展,就可以直接使用子类功能。
构造方法 概念:
与类同名,且没有返回值类型,可以含参也可以不含参
构造方法是一种特殊的方法,它是一个与类同名且没有返回值类型的方法
构造方法的主要功能就是完成对象创建或者初始化
当类创建对象(实例化)时,就会自动调用构造方法
构造方法与普通方法一样也可以重载.
构造方法可以被继承吗?--不可以!!!!
语法结构的要求:构造方法的方法名需要与本类类名一致,不然就不符合要求.
默认无参构造和普通方法和构造方法的区分:
1.默认存在无参构造,当new Person()会自动触发此无参构造.
2.普通方法的定义:修饰符 返回值类型 方法名 (参数列表){ 方法体 }.
3.构造方法的定义:修饰符 方法名 (参数列表){ 方法体 } --方法名与类名一致
3.1)构造方法也存在重载的现象:方法的重载:在同一个类中,方法名相同且参数列表不同的现象
4.当只提供了含参构造,默认提供的无参构造会被覆盖,所以在创建重载的构造方法时,一定注意要手动添加无参构造
构造函数怎么记忆:
特点:方法名与类名相同,且没有返回值类型
执行时机:创建对象时立即执行
默认会创建无参构造,但是,如果自定义了含参构造,默认的无参构造会被覆盖,注意要手动添加哦
生成无参构造快捷键与生成全参构造快捷键:
无参:ALT+INSERT 选择Constructor -> Select None ->OK
全参:ALT+INSERT 选择Constructor -> Select CTRL+a全选 - > OK
private关键字:
是一个权限修饰符 ,可以用来修饰成员变量和成员方法.被私有化的成员只能在本类中访问.
继承以后,子类就拥有了父类的功能
在子类中,可以添加子类特有的功能,也可以修改父类的原有功能
子类中方法的签名与父类完全一样时,会发生覆盖/复写的现象
格式要求:方法的返回值 方法名 参数列表 要完全一致,就方法体是重写的
注意: 父类的私有方法不能被重写,子类在重写父类方法时,修饰符
子类重写父类方法时,子类修饰符要大于等于父类修饰符的权限
OPC原则:
OCP原则:面向修改关闭,面向拓展开放--只允许功能拓展,不允许修改原来的代码
这个功能的修改被称作方法的重写
1.重写:和父类的方法签名[返回值类型&方法名&参数列表]保持一致
修改的是子类的功能,父类的功能并没有发生任何的改变.
2.重写时,子类必须有权限去重写父类的功能,父类的私有方法无法被重写
子类的方法修饰符权限>=父类被重写方法的修饰符的权限
总结:
1.方法重写的意义:是在不修改源代码的前提下,完成业务的修改
2.重写的要求:子类的方法声明(返回值类型 方法名(参数列表) )和父类的一模一样
3.重写并没有改变父类原有的业务,只是子类的业务进行了修改
如何判断他是否是重写:
@Override//注解,标记这个方法是实现父类接口中未实现的抽象方法
父类中的xxx方法上的一个小标签
如果他是一个重写的方法,他就让你写,如果不是他就不让你写
重载: 是指在一个类中的现象,是指一个类中有很多同名的方法,但是方法的参数列表不同
重写: 是指发生了继承关系以后(两个类),子类去修改父类原有的功能
语法规则:子类的方法签名(返回值类型 方法名(参数列表) )与父类一致
重写方法的修饰符: 子类权限 >= 父类的权限
重载的意义: 是为了方便外界对方法进行调用,什么样的参数程序都可以找到对应的方法来执行,体现的是程序的灵活性
重写的意义:是在不修改源码的前提下,进行功能的修改和拓展
(OCP原则:面向修改关闭,面向拓展开放
This代表本类对象的引用
class Cat{ this.XXX } 可以理解成: Cat this = new Cat();
super代表父类对象的引用
class Father{ }
class Son extends Father{ super.XXX }
可以理解成: Father super = new Father();
this可以在两个变量名相同时,用于区分成员变量和局部变量
this 可以在本类的构造方法之间调用,位置必须是第一条语句,注意,不能相互调用,会死循环
super是发生了继承关系以后,子类如果想用父类的功能,可以通过super调用
如果发生了重写,还想用父类的功能,需要使用super来调用
Super在调用父类构造方法时,必须出现在子类构造方法的第一条语句,而且如果父类中没有提供无参构造,子类可以通过super来调用父类其他的含参构造
static是java中的一个关键字
用于修饰成员(成员变量和成员方法)
特点:
可以修饰成员变量与成员方法
随着类的加载而加载,优先于对象加载
只加载一次,就会一直存在,不再开辟新空间, 直到类消失才一起消失
静态资源也叫做类资源,全局唯一,被全局所有对象共享
可以直接被类名调用
静态只能调用静态,非静态可以随意调用
static不能和this或者super共用,因为有static时可能还没有对象
{ 构造代码块的位置:类里方法外 作用:用于提取构造方法中的共性内容 执行时机:优先于构造方法执行*/ subject = "Java培优"; System.out.println("构造代码块"); }
每次创建对象时,都会执行一次构造代码块和构造方法,并且构造代码块优先于构造方法执行
特点:
- 位置: 在类的内部,在方法的外部
- 作用: 用于抽取构造方法中的共性代码
- 执行时机: 每次调用构造方法前都会调用构造代码块
- 注意事项: 构造代码块优先于构造方法加载
总结:
构造方法:与类同名且没有返回值类型的用来创建对象的方法(还可以赋值).
构造代码块:类里方法外用来提取构造方法公性功能的代码块
局部代码块:写在方法里用来控制变量范围的代码块
- 位置: 在方法里面的代码块
- 作用: 通常用于控制变量的作用范围,出了花括号就失效
- 注意事项:变量的作用范围越小越好,成员变量会存在线程安全的问题
演示:
//.定义普通方法
//方法定义:修饰符 返回值类型 方法名(参数列表){方法体...}
public void study() {
//创建局部代码块
局部代码块的位置:方法里
作用:控制变量的作用范围(作用范围越小越好,因为越小越安全)
执行时机:调用本方法时
{
int i = 10;
System.out.println(i);
}
//System.out.println(i);//报错,因为超出i的作用范围
System.out.println("正在备课...");
}
定义:
静态资源随着类的加载而加载,并且只被加载一次,一般用于项目的初始化
特点: 被static修饰,位置在类里方法外
总结:
代码块之间的执行顺序:
静态代码块-->构造代码块-->构造方法-->局部代码块
为什么是这样的顺序:
静态代码块要优先于对象进行加载,是随着类的加载而加载到内存中的
静态资源只加载一次就会一直存在,直到类的消失,它才会消失
每个元素的作用:
静态代码块:专门用来完成一些需要在第一时间加载并且只加载一次的资源
构造代码块:创建对象时才会触发,用来提取构造方法中的共性内容
构造方法:创建对象时调用,用来创建对象,在构造代码块之后执行
局部代码块:调用其所在方法时才会执行,用于控制变量的作用范围.
在继承当中这些顺序是什么样的?它们的执行顺序是什么样的?
注意:静态优先于普通,父类优先于子类
父类的静态代码块
子类的静态代码块
父类的构造代码块
子类的构造代码块
父类的构造方法
子类的构造方法
父类的普通方法
子类的普通方法
父类的局部代码块
子类的局部代码块
Fiyst in = olnt
创建对象时内存经历了什么?
1.在栈内存中开辟了一块空间,存放引用类型变量P,并把P压入栈底
2.在推内存中开辟了一块空间,存放Phone对象
3.完成对象初始化,并赋值默认值
4.给初始化完毕的对象赋唯一的地址值
5.把地址值交给引用类型变量P来保存
1.抽象类中可以有成员变量吗?--可以!!!
2.抽象类中可以有成员常量了吗?----可以!!!! 创建抽象父类中的成员常量,注意要初始化
3.抽象类中可以有普通方法吗?---可以!!
抽象类中可以都是普通方法吗? ---可以!!!如果一个类中都是普通方法,为啥还要被声明抽象类呢?
原因:抽象类不可以创建对象
所以如果不想让外界创建不类的对象,就可以把普通类声明抽象类
向上抽取形成父类,那么子类继承父类以后必须重写或实现父类方法
1.没有方法体的方法叫做抽象方法,被abstract关键字修饰
2.用关键字abstract修饰的类称为抽象类,
如果一个类中包含抽象方法,那么这个类必须声明抽象类
方案一:
选择不实现父类的抽象方法,子类也变成抽象类
abstract class pig extends Animal{
方案二:
作为一个普通子类实现父类的所有抽象方法
class pig extends Animal{ //实现父类的抽象方法 @Override//注解,相当于标记,标记这是个重写的方法 public void fly() { System.out.println("我终于把我爸钱还完了,~"); }
当一个类继承了父类,并且父类是抽象父类时,子类需要重写(实现)父类的所有抽象方法或者把自己变成抽象子类
抽象类不可以创建对象(实例化~~)
当你创建一个类,不想被别人用时,可以加abstract, 反过来想.
不能在创建父类抽象方法自己的对象,不能实例化,
----可以创建多态对象 父类 a = new 子类
抽象方法要求子类继承后必须重写。
那么,abstract关键字不可以和哪些关键字一起使用呢?以下关键字,在抽象类中。用是可以用的,只是没有意义了。
1.private:被私有化后,子类无法重写,与abstract相违背。
2.static:静态优先于对象存在,存在加载顺序问题。
3.final:被final修饰后,无法重写,与abstract相违背
在抽象类中可以有构造方法
父类构造方法优先于子类执行
抽象类不可以实例化(创建对象)
抽象类中存在构造方法不是为了创建本类对象时调用的是为了创建子类对象时调用
概念:
- 是java提供的一个关键字
- final是最终的意思
- final可以修饰类,方法,成员变量
- 初衷:java出现继承后,子类可以更改父类的功能,当父类功能不许子类改变时,可以利用final关键字修饰父类。
特点:
- 被final修饰的类,不能被继承
- 被final修饰的方法,不能被重写
- 被final修饰的变量是个常量,值不能被改变
- 常量的定义形式:final 数据类型 常量名 = 值
final是JAVA中的一个关键字,表示最终的意思,final可以修饰类,可以修饰变量,,可以修饰方法,那么被final的类是最终类不可被继承,被final的变量是常量值不可被修改,被final修饰的方法是这个方法的最终实现不可被重写
创建抽象父类中的成员常量,注意要初始化
与之前学习过的抽象类一样,接口( Interface )在Java中也是一种抽象类型,接口中的内容是抽象形成的需要实现的功能,接口更像是一种规则和一套标准.
实现类如果想和接口建立实现关系,通过implements关键字来建立
方案一:如果实现类与接口建立关系以后,可以选择不实现接口中的抽象方法,而是把自己变成抽象类
abstract public class InterImpl implements Inter
方案二:实现类可以实现接口中的所有抽象方法
@Override//注解,标记这个方法是实现父类接口中未实现的抽象方法
第三个去用,(测试,main方法)
接口可以创建对象吗?---不可以!!!
接口中有构造方法吗?--没有!!
接口中可以有成员变量吗?--不可以!!!
这其实是一个静态常量,实际上的是写法是public static final int a = 10;
接口中的是静态常量,只不过前面的元素默认拼接,可以不用自己写
1.通过interface关键字来定义接口
2.接口中可以有普通方法吗?----不可以
3.接口中可以有抽象方法吗?----可以,接口中的方法都是抽象方法
总结:
结论:接口中的变量实际都是静态变量,可以被类名直接调用
System....(类名.age);
结论:接口中的变量实际上都是静态变量,值不可以被修改
类名.age = 100;
问题:子类创建对象时,默认会调用父级的无参构造,目前接口实现类的父级是一个接口,而接口没有构造方法,那实现类构造方法中的super()调用的是谁呢?
结论:如果一个类没有明确指定父类,那么默认继承顶级父类Object所以super()会自动调用Object类中的无参构造
抽象类是一个特殊的类,特殊在,抽象类中可以包含没有方法体的方法(抽象方法)
接口可以理解成一个特殊的抽象类,特殊在,接口里的都是抽象方法,没有普通方法
接口会为方法自动拼接public abstract,还会为变量自动拼接public final static
抽象类可以有构造方法–用来给子类创建对象,接口中没有构造方法
抽象类和接口都不能实例化(创建对象)
接口可继承接口,并可多继承接口,但类只能单继承
抽象方法只能声明,不能实现,接口是设计的结果 ,抽象类是重构的结果
是继承关系,可以单继承,也可以多继承
interface A extends B,C{}
其中ABC都是接口,A是子接口,具有BC接口的所有功能(抽象方法)
class X implements A{}
X实现类需要重写ABC接口的所有方法,否则就是抽象类
class A extends B implements C,D{}
其中A是实现类,也是B的子类,同时拥有CD接口的所有功能
这时A需要重写CD接口里的所有抽象方法
实现关系.可以单实现,也可以多实现
class A implements B,C{}
其中A是实现类,B和C是接口,A拥有BC接口的所有功能,只是需要进行方法的重写,否则A就是抽象类
接口和实现类之间可以建立实现关系,
通过implements关键字来完成,
注意,JAVA是单继承,而接口不限,写接口时,
我们一般先继承在实现
接口之间可以建立继承关系,
而且还可以多继承,
接口与接口之间用逗号隔开
总结:
接口里没有成员变量,都是常量。
所以,你定义一个变量没有写修饰符时,
默认会加上:public static final
总结:
总结:
接口里是没有构造方法的.
在创建实现类的对象时默认的super(),是调用的默认Object的无参构造
继承关系,只支持单继承
比如,A是子类 B是父类,A具备B所有的功能(除了父类的私有资源和构造方法)
子类如果要修改原有功能,需要重写(方法签名与父类一致 + 权限修饰符>=父类修饰符)
面向过程--思想--干活的主体是自己
面向对象--思想--干活不是自己,我们是指挥者,不论干啥,都得先创建对象
把一类事物抽象成一个类型--模板/图纸--class
根据图纸创造出来的一个个实际的实例--new
一共分5块,目前我们需要了解的是栈(先进后出)和堆(对象都存入堆中)
1)抽取形成类: 如:Phone/Student/Teacher/Person
2)属性: 成员变量,属性还可以封装并提供get/set方法
3)静态代码块: 类加载的时候加载,只加载一次
4)构造代码块:如果构造方法有需要提取的共性内容,可以写构造代码块
5)构造方法: 创建对象(还可以给对象的属性值赋值),默认存在无参构造
6)普通方法/成员方法: 完成一些业务功能(行为)
7)局部代码块: 写在方法中的代码块,调用此方法时才执行
1)封装 :private,可以封装属性和方法,为了限制(按照我们指定的方式)外界的访问
2)继承 :extends,子类复制父类的所有内容,私有资源不可见,单继承
3)多态 : 忽略子类差异,提供通用解决方案
--花木兰替父从军Animal a = new Cat();
向下造型(把之前向上造型的子类对象看回成子类类型)
--花木兰解甲归田Cat c = (Cat)a;
多态最主要的作用就是提供通用解决方案
--不管是什么子异常,统一用Exception处理
构造方法:
与类同名且没有返回值类型的用来创建对象的方法(还可以赋值).
构造代码块:
类里方法外用来提取构造方法共性功能的代码块.
局部代码块:
写在方法里用来控制变量范围的代码块.
1)在成员变量与局部变量同名时,可以用来指定成员变量
2)this可以实现构造方法间的调用,但是不可以互相调用
this();--调用本类无参构造 this(" ");--调用本类的含参构造
super表示父类对象的一个引用,如果出现了同名资源,可以通过super指定
super();--调用父类无参构造 super(" ");--调用父类的含参构造
1) 优先加载,随着类的加载而加载,类消失才会消失,只加载一次,优先于对象
2) 可以被类名直接调用,只有一份,被全局所有对象共享
3) 静态资源只能调用静态资源
1)类 : 不可被继承
2)属性 : 值不可被修改--常量
3)方法 : 不可以被重写
重载: 同一个类 方法名相同 参数列表不同(个数,对应位置的参数类型)
重写: 两个类(父子类) 方法签名一致,子修饰符>=父修饰符
重写是为了修改/拓展父类的功能,可以加注解@Override
1).异常的继承结构Throwable Error(不可解决) Exception(编码问题,可以解决)
2).异常的处理方法:
捕获:try-catch,catch可嵌套
向上抛出:throws
如果有方法抛出异常,那调用这个方法的方法也必须解决(捕获/抛出)这个异常
异常不可以抛给main(),因为没人解决了
1.可以修饰类 --称为抽象类,不可以被实例化,有构造,为了子类创建对象使用
2.可以修饰方法 --方法没有方法体
3.子类继承抽象父类后:1)变成抽象子类 2)实现抽象父类所有抽象方法
4.不能与static final private共用
Matches(正则) : 当前字符串能否匹配正则表达式
replaceAll(正则,子串) : 替换子串
split(正则) : 拆分字符串
- 如果是第一次使用字符串,java会在字符串堆中常量池创建一个对象。
- 再次使用相同的内容时,会直接访问堆中常量池中存在的对象。
System.out.println(s);//abc,串s本身的值没有被影响 System.out.println(s.endsWith("y"));//false,判断是否以指定元素结尾
System.out.println(String.valueOf(10)+10);//1010,把int10转成成String类型
length()-查看字符串的长度
charAt()—获取指定下标处位置上的字符
lastIndexOf()-某个字符最后一次出现的位置
substring()-截取子串,如果参数有两个左闭右开[1,5)
equals()-判断两个串是否相等,注意String重写了Object的此方法,所以内容相同就返回true
startsWith()-判断是不是以参数开头
endsWith()–判断是不是以参数结尾
split()—以指定字符分割
trim()-去掉首尾两端的空格
getBytes()-把串转换成数组
toUpperCase()-变成全大写
toLowerCase()-变成全小写
String.valueOf(10)-把int类型的10转换成String类型
Integer:
方式一: new Integer(5);
方式二: Integer.valueOf(5); 高效效果
Integer类中包含256个Integer缓存对象,范围是 -128~127
使用valueOf()时,如果指定范围内的值,直接访问缓存对象不新建;如果指定范围外的值,直接新建对象。
常见方法:
static int parseInt(String s) 将字符串参数作为有符号的十进制整数进行解析
原因:parseInt()已经把字符串800转成了int类型的800,所以是运算而不是拼接
Double:
Double d1 = new Double(3.14); Double d2 = Double.valueOf(3.14); Double d3 = Double.valueOf(3.14); System.out.println(d1 == d2);//false /*只有Integer才有"高效"的效果*/ System.out.println(d2 == d3);//false
常用方法:
Double.parseDouble();
自动装箱:
2.现在的方式: 1.自动装箱:编译器会自动把基本类型int 5,包装成包装类型Integer 然后交给i3来保存,自动装箱底层发生的代码Integer.valueOf(5);因为高效 valueOf()的方向: int --> Integer*/ Integer i3 = 5;//不会报错,这个现象就是自动装箱
理解:
自动装箱 把基本类型包装成包装类型,
什么方法?
Integer a = Integer.valueOf(5);
自动拆箱:
2.自动拆箱:编译器会自动把包装类型的i1拆掉"箱子",变回基本类型数据127 然后交给i4来保存,自动拆箱底层发生的代码:i1.intValue(); intValue()的方向:Integer -> int int i4 = i1;//不会报错,这个现象就是自动拆箱
理解:
自动拆箱 从包装类型 变成基本类型
什么方法?
int i = a.intValue();
创建对象:
方式一 :
BigDecimal(double val)
将double转换为BigDecimal,后者是double的二进制浮点值十进制表示形式,有坑!
方式二 :
BigDecimal(String val)
将String类型字符串的形式转换为BigDecimal
1.最好不要用double作为构造函数的参数,不然还会有不精确的现象,有坑!!!
2.最好使用重载的,参数类型是String的构造函数
double转String,直接拼个空串就可以
Add(BigDecimal bd) : 做加法运算
Subtract(BigDecimal bd) : 做减法运算
Multiply(BigDecimal bd) : 做乘法运算
Divide(BigDecimal bd) : 做除法运算,除不尽时会抛异常
Divide(BigDecimal bd,保留位数,舍入方式) : 除不尽时使用
setScale(保留位数,舍入方式) : 同上
pow(int n) : 求数据的几次幂
:(除不尽时有问题) bd3 = bd1.divide(bd2);
divide(m,n,o)
m是要除以哪个对象,n指要保留几位,o指舍入方式(比如四舍五入)
//方案二:
bd3 = bd1.divide(bd2,3,BigDecimal.ROUND_HALF_UP);
System.out.println(bd3);
舍入方式解析
ROUND_HALF_UP 四舍五入,五入 如:4.4结果是4; 4.5结果是5
ROUND_HALF_DOWN 五舍六入,五不入 如:4.5结果是4; 4.6结果是5
ROUND_HALF_EVEN 公平舍入(银行常用)
比如:在5和6之间,靠近5就舍弃成5,靠近6就进位成6,如果是5.5,就找偶数,变成6
ROUND_UP 直接进位,不算0.1还是0.9,都进位
ROUND_DOWN 直接舍弃,不算0.1还是0.9,都舍弃
ROUND_CEILING(天花板) 向上取整,取实际值的大值
朝正无穷方向round 如果为正数,行为和round_up一样,如果为负数,行为和round_down一样
ROUND_FLOOR(地板) 向下取整,取实际值的小值
朝负无穷方向round 如果为正数,行为和round_down一样,如果为负数,行为和round_up一样
api是一些预先定义好的函数.
1) 程序员无需理解其内部机制和细节,就可以使用其功能
2) api也作为规则,面向接口开发
每个类都使用Object作为超类,也就是我们所说的"顶级父类"
当一个类没有明确指定父类时,默认以Object作为其父类
1)toString():默认实现返回的是地址值,重写后打印 对象类型+属性值
2)hashCode():返回对应对象的哈希码值
3)equals():默认实现==比较,比较的是地址值
3.1)Student重写后比较的是类型+所有属性值一致就返回true
3.2)String默认重写了equlas(),它比较的是两个串的具体内容需求:我们想如果两个对象的类型相同,并且所有属性值也相同,就返回true
String底层是char[]
1)准备char数组存放数据,然后将这个数组传给String的构造函数创建对象
2)String s = "abc";此种有高效的效果,在堆中常量池
首次创建时新建,第二次不再新建,到常量池中找数据直接使用
3)相关方法的使用
4)StringBuilder与StringBuffer对String+拼接的优化--append()5)两种创建方式
6)常用方法
7.StringBuilder/StringBuffer
append()--拼接效率高
1.流只能单方向流动
2.输入流用来读取 → in
3.输出流用来写出 → out
4.数据只能从头到尾顺序的读写一次
所以以程序的角度来思考,In/out 相对于程序而言的输入(读取)/输出(写出)的过程.
字节流 : 针对二进制文件:
File 文件流
字节流:针对二进制文件
InputStream 字节输入流
FileInputStream 文件字节输入流
BufferedInputStream 缓冲字节输入流
ObjectInputStream 反序列化
OutputStream 字节输出流
FileOutputStream 文件直接输出流
BufferedOutputStream 缓冲字节输出流
ObjectOutputStream 序列化
字符流 : 针对文本文件,读写容易出现乱码的现象,在读写时,最好指定编码集为UTF-8
在结合对应类型的输入和输出方向,常用的流有:
字符流:针对文本文件
Reader 字符输入流
FileReader 文件字符输入流
BufferedReader 缓冲字符输入流
InputStreamReader
Writer 字符输出流
FileWriter 文件字符输出流
BufferedWriter 缓冲字符输出流
OutputStreamWriter
PrintWriter一行行写出
概述:
封装一个磁盘路径字符串,对这个路径可以执行一次操作
可以封装文件路径、文件夹路径、不存在的路径
File(String pathname)通过将给定路径名字符串转换为抽象路径名来创建一个新的File实例
new File(“d:/abc/a.txt”);
new File(“d:/abc”,”a.txt”);
1.创建File文件对象 1.构造函数的参数是String类型的pathname(路径名) 这个路径可以是文件路径,也可以是文件夹路径*/ 2.\在代码中有特殊的意义,所以想要真正表示这是一个\,需要用\进行转义*/ //注意:此处需要自己手动在D盘创建对应目录下的1.txt,并添加内容 //注意:创建1.txt时,需要设置系统显示文件后缀名,如果没设置,文件名应该是1 //注意:File需要导包:import java.io.File; File file = new File("D:\\ready\\1.txt");
System.out.println(file.length());//12,获取指定文件的字节量大小
System.out.println(file.exists());//true,判断指定文件是否存在
System.out.println(file.isFile());//true,判断指定内容是否为文件
System.out.println(file.isDirectory());//false,判断指定内容是否为文件夹
System.out.println(file.getName());//1.txt,获取指定内容的名字
System.out.println(file.getParent());//D:\ready,获取指定内容的上级
System.out.println(file.getAbsolutePath());//D:\ready\1.txt,获取指定内容的绝对路径
file = new File("D:\\ready\\2.txt");
如果指定创建文件的文件夹不存在,会报错:java.io.IOException
系统找不到指定的路径,由于可能会发生异常,所以调用时需要抛出异常 */
在windows中创建不存在的文件2.txt,成功返回true
System.out.println(file.createNewFile());//true
file = new File("D:\\ready\\m");
System.out.println(file.mkdir());//true,创建不存在的单层文件夹m
file = new File("D:\\ready\\a\\b\\c");
System.out.println(file.mkdirs());//true,创建不存在的多层文件夹a/b/c
System.out.println(file.delete());//c被删除,删除文件或者空文件夹
file = new File("D:\\ready\\a");
System.out.println(file.delete());//false,因为a目录里还有b目录
file = new File("D:\\ready\\2.txt");
System.out.println(file.delete());//true,删除2.txt文件成功
final:是修饰符,修饰类不能被继承,
修饰方法不能被重写
修饰变量是常量,不能被修改
finally是try-catch结构中的一个代码块放在这个部分的代码是一定被执行
1.创建流
2.使用流写出数据
2.2刷新一下数据
out.flush();
3.释放资源
out.colse();
流资源必须释放,释放的是之前使用过程中所有的流对象,
关流是有顺序的,注意,后创建的流先关闭,为了不影响其他的代码,不能先关最先创建的流
一,用什么流都要导包
二,发生异常,捕获异常
如果要保证代码一定能执行,就通通放在finally代码块中
此抽象类是表示输出字节流的所有类的超类.输出流接受输出字节并将这些字节发送到某个接收器.
常用方法:
Void close() 关闭此输出流并释放与此流相关的所有系统资源
Void flush() 刷新此输出流并强制写出所有缓冲的输出字节
Void write(byte[ ] b) 将b.length个字节从指定的byte数组写入此输出流
Void write(byte[ ] b,int off ,int len) 将指定byte数组中从偏移量off开始的len个字节写入输出流
Abstract void write(int b) 将指定的字节写入此输出流
out.write(int类型);
注意:要强制刷新一下输出流,方式遗漏数据
直接插在文件上,直接写出文件数据
构造方法(创建对象):
FileOutputStream(String name)
创建一个向具有指定名称的文件中写入数据的文件输出流
FileOutStream(File file)
创建一个向指定File对象表示的文件中写入数据的文件输出流
FileOutStream(File file,boolean append)—如果第二个参数为true,表示追加,不覆盖
创建一个向指定File对象表示的文件中写入数据的文件输出流,后面的参数是指是否覆盖原文件内容
该类实现缓冲的输出流,通过设置这种输出流,应用程序就可以将各个字节写入底层输出流中,而不必每次针对字节写出调用底层系统
构造方法(创建对象):
BufferedOutputStream(OutputStream out)
创建一个新的缓冲输出流,用以将数据写入指定的底层输出流
写入字符流的抽象类
Abstract void close() 关闭此流,但要先刷新它
Void write(char[ ] cbuf) 写入字符数组
Void write(int c) 写入单个字符
Void write(String str) 写入字符串
Void write(String str,int off,int len) 写入字符串的某一部分
Abstract void write(char[] cbuf,int off,int len)写入字符数组的某一部分
用来写入字符文件的便捷类,此类的构造方法假定默认字符编码和默认字节缓冲区大小都是可接受的.如果需要自己自定义这些值,可以先在FileOutputStream上构造一个OutputStreamWriter.
构造方法(创建对象):
FileWriter(String filename)
根据给定的文件名构造一个FileWriter对象
FileWriter(String filename,boolean append)
根据给定的文件名以及指示是否附加写入数据的boolean值来构造FileWriter
将文本写入字符输出流,缓冲各个字符,从而提供单个字符,数组和字符串的高效写入.可以指定缓冲区的大小,或者接受默认的大小,在大多数情况下,默认值就足够大了
构造方法(创建对象):
BufferedWriter(Writer out)
创建一个使用默认大小输出缓冲区的缓冲字符输出流
此抽象类是表示字节输入流的所有类的超类/抽象类,不可创建对象哦
abstract int read() 从输入流中读取数据的下一个字节
int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中
int read(byte[] b, int off, int len) 将输入流中最多 len 个数据字节读入 byte 数组,off表示存时的偏移量
void close() 关闭此输入流并释放与该流关联的所有系统资源
InputStream是字节输入流的抽象父类,不能实例化
InputStream in = new InputStream();
try-catch:try中放的是可能会发生异常的代码. catch匹配并捕获异常,如果捕获到异常,就去解决
通用方法:
in.read() 每次读取一个字符,如读到了数据的末尾,返回-1
释放资源:
in.close();
直接插在文件上,直接读取文件数据
创建对象:
FileInputStream(File file)—直接传文件对象
通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的 File 对象 file 指定FileInputStream(String pathname)—传路径
通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name 指定
高效的字节流读取对象
创建对象不一样:
InputStream in2 = new BufferedInputStream ( new FileInputStream("文件位置"))
创建对象:
BufferedInputStream(InputStream in)
创建一个 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用
BufferedInputStream 为另一个输入流添加一些功能,即缓冲输入以及支持 mark 和 reset 方法的能力。在创建 BufferedInputStream 时,会创建一个内部缓冲区数组(默认8k大小)。在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。
用于读取字符流的抽象类。
常用方法:
int read() 读取单个字符
int read(char[] cbuf) 将字符读入数组
abstract int read(char[] cbuf, int off, int len) 将字符读入数组的某一部分
int read(CharBuffer target) 试图将字符读入指定的字符缓冲区
abstract void close() 关闭该流并释放与之关联的所有资源
用来读取字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是适当的。要自己指定这些值,可以先在 FileInputStream 上构造一个 InputStreamReader。
创建对象:
FileReader(String fileName) 在给定从中读取数据的文件名的情况下创建一个新 FileReader
FileReader(File file) 在给定从中读取数据的 File 的情况下创建一个新 FileReader
从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。
创建对象
BufferedReader(Reader in) 创建一个使用默认大小输入缓冲区的缓冲字符输入
概述:
序列化是指将对象的状态信息转换为可以存储或传输形式的过程.在序列化期间,对象将其当前状态写入到临时或持久性存储区.以后可以通过从存储区中读取或者反序列化对象的状态,重新创建该对象.
1.利用ObjectOutputStream,把对象的信息,按照固定的格式转成一串字节值输出并持久保存到磁盘
2.本类如果想要实现序列化,必须实现可序列化接口,否则会报错NotSerializableException: cn.tedu.seri.Student
2.1)Serializable是一个空接口,里面一个方法都没有,作用:当做标志,标志着这个类可以被序列化
2.2)需要给每个进行序列化的文件分配唯一的UID值
private static final long serialVersionUID = 1L;m'r 默认存在的
out.writeObject(Student对象)
1.利用ObjectInputStream,读取磁盘中之前序列化好的数据,重新恢复成对象
2.打印结果:cn.tedu.seri.Student@6d30e736
想看对象的属性值,需要重写toString()
ALT+INSERT 选toString全选
报错原因:本次反序列化时使用的UID与序列化时的UID不匹配 解决方案:反序列化时的UID与序列化时的UID要保持一致,或者测试时一次序列操作对应一次反序列化操作,否则不匹配就报错
序列化:ObjectOutputStream
ObjectOutputStream 将 Java 对象的基本数据类型写入 OutputStream,通过在流中使用文件可以实现对象的持久存储。如果流是网络套接字流,则可以在另一台主机上或另一个进程中重构对象。
构造方法:
构造方法:
ObjectOutputStream(OutputStream out)
创建写入指定 OutputStream 的 ObjectOutputStream
普通方法:
writeObject(Object obj)
将指定的对象写入 ObjectOutputStream
反序列化:ObjectInputStream
ObjectInputStream对以前使用ObjectOutputStream写入的基本数据和对象进行反序列化重构对象。
构造方法:
构造方法:
ObjectInputStream(InputStream in) 创建从指定 InputStream 读取的 ObjectInputStream
普通方法:
readObject() 从 ObjectInputStream 读取对象
我们在反序列化时,JVM会拿着反序列化流中的serialVersionUID与序列化时相应的实体类中的serialVersionUID来比较,如果不一致,就无法正常反序列化,出现序列化版本不一致的异常InvalidClassException。
而且我们在定义需要序列化的实体类时,如果没有手动添加UID,
Java序列化机制会根据编译的class自动生成一个,那么只有同一次编译生成的class才是一样的UID。
如果我们手动添加了UID,只要这个值不修改,就可以不论编译次数,进行序列化和反序列化操作。
我们接下来要学习的内容是Java基础中一个很重要的部分:集合
为了更好的理解集合,我们需要首先引入一个概念:泛型
其实就是< ? >的部分,它就是泛型
泛型是(Generics)JDK1.5 的一个新特性,通常用来和集合对象一起使用
泛型概念非常重要,它是程序的增强器,它是目前主流的开发方式
那泛型有什么作用呢?
1.我们可以把泛型理解成一个“语法糖”,本质上就是编译器为了提供更好的可读性而提供的一种小手段,小技巧,虚拟机层面是不存在所谓“泛型”的概念的。是不有点神奇,不知所云,别着急等我讲完你就清楚了。
作用2:
我们可以通过泛型的语法定义<>,来约束集合中元素的类型,编译器可以在编译期根据泛型约束提供一定的类型安全检查,这样可以避免程序运行时才暴露BUG,代码的通用性也会更强
泛型可以提升程序代码的可读性,但是它只是一个“语法糖”(编译后这样的部分会被删除,不出现在最终的源码中),所以不会影响JVM后续运行时的性能.
引入泛型--主要目的是想通过泛型来约束集合中元素的类型>
泛型的好处:可以把报错的时机提前,在编译期就报错,而不是运行后抛出异常
在向集合中添加元素时,会先检查元素的数据类型,不是要求的类型就编译失败
示例1 : 我们创建一个ArrayList,看到eclipse发出黄线警告,这是为什么呢?
原因:ArrayList定义时使用了泛型,在声明时需要指定具体的类型
我们把这个”<>”的方式称之为泛型,那么泛型有什么样的作用呢?就是在编译阶段检查传入的参数是否正确
有了泛型,我们可以看到要求存放的是String类型,而测试时存放的是int类型的100,所以编译器报错:
类型List的add方法要求添加的类型为String类型,int类型不匹配,不能正确存入
private static
void print(E[] a) { for (E s: a ){ System.out.println(s); } }
1.泛型可以实现通用代码的编写,使用E表示元素的类型是Element类型 -- 可以理解成神似多态
2.泛型的语法要求:如果在方法上使用泛型,必须两处同时出现,一个是传入参数的类型,一个是返回值前的泛型类型,表示这是一个泛型
在方法的返回值前声明了一个,表示后面出现的E是泛型,而不是普通的java变量
泛型是怎么来的?--想要模拟数组的数据类型检查
没有泛型,数据类型根本没有约束 -- 太自由!!!
--type的值应该如何写?
需要查看要存放的数据类型是什么,根据类型进行定义
但是type必须是引用类型,不是基本类型可以用Integer -- 包装类
提前检查,限制类型. 可以写出通用的代码
高效for/foreach循环--如果只是单纯的从头到尾的遍历,使用增强for循环
好处:比普通的for循环写法简便,而且效率高
坏处:没有办法按照下标来操作值,只能从头到尾依次遍历
语法:for(1 2 : 3){代码块}
3是要遍历的数据
1是遍历后得到的数据的类型
2是遍历起的数据名
for (String s: b ) {
System.out.println(s);
}
普通循环的好处是可以控制循环的步长(怎么变化)
Collection接口
List 接口
【数据有下标,有序,可重复】ArrayList子类
LinkedList子类
Set 接口
【数据无下标,无序,不可重复】HashSet子类
Map 接口
【键值对的方式寸数据】HashMap子类
集合的英文名称是Collection,是用来存放对象的数据结构,而且长度可变,可以存放不同类型的对象,并且还提供了一组操作成批对象的方法.Collection接口层次结构 中的根接口,接口不能直接使用,但是该接口提供了添加元素/删除元素/管理元素的父接口公共方法.
由于List接口与Set接口都继承了Collection接口,因此这些方法对于List集合和Set集合是通用的.
Java语言的java.util包中提供了一些集合类,这些集合类又称之为容器
提到容器不难想到数组,集合类与数组最主要的不同之处是,数组的长度是固定的,集合的长度是可变的,而数组的访问方式比较单一,插入/删除等操作比较繁琐,而集合的访问方式比较灵活
常用的集合类有List集合,
Set集合,
Map集合,
其中List集合与Set集合继承了Collection接口
各个接口还提供了不同的实现类.
是泛型,用来约束集合中的元素类型,只能写引用类型,不能写基本类型
System.out.println( c.contains(300) );//true,判断集合中是否包含元素300
System.out.println( c.hashCode() );//127240651,返回集合对应的哈希码值
System.out.println( c.isEmpty() );//false,判断集合是否为空
System.out.println( c.remove(100) );//true,移出集合中的元素100,移出成功返回true
System.out.println( c );//[200, 300, 400, 500],100被成功移除
System.out.println( c.size() );//4,获取集合的元素个数/类似数组长度
System.out.println( c.equals(200) );//false,判断是否与100相等
/**接返回值类型快捷键:Shift+alt+L*/
Object[] array = c.toArray();//把集合中的元素放入数组
System.out.println(Arrays.toString(array));//使用数组的工具类查看数组中的元素内容
System.out.println(c.conta ins(c2));//false ,查看c集合是否包含一个叫c2的元素
System.out.println(c.retainAll(c2));取c集合中属于c2集合的所有元素"交集"
System.out.println(c.containsAll(c2));//true,,查看c集合是否包含c2集合的所有元素
System.out.println(c.removeAll(c2));//true,删除c集合中属于c2集合的所有元素
有序的colletion(也称为序列).此接口的用户可以对列表中的每个元素的插入位置进行精确的控制,用户可以根据元素的整数索引(在列表中的位置)来访问元素,并搜索列表中的元素.
- 元素都有下标
- 数据是有序的
- 允许存放重复的元素
list.add("美女"); System.out.println(list);不指定位置就放在最后 list.add(0,"黑丝袜"); System.out.println(list);在指定索引处添加元素,第一个位置下标为0
System.out.println(list); System.out.println(list.get(3));获取指定位置元素 System.out.println(list.indexOf("黑丝袜"));判断指定元素的索引 System.out.println(list.lastIndexOf("美女"));判断指定元素最后出现的索引
System.out.println(list.remove(6));根据索引位置移出指定元素 System.out.println(list); System.out.println(list.set(5,"白丝袜"));重置指定下标的元素值 System.out.println(list);
List
list1 = list.subList(1,5);截取字串,含头不含尾{1,5) System.out.println(list1);
System.out.println( list.addAll(list2) );//把list2集合添加到list集合中 System.out.println( list.addAll(1,list2) );//把list2集合添加到list集合的指定位置处 System.out.println( list ); System.out.println( list.containsAll(list2) );//判断list集合是否有一个元素是list2 System.out.println( list.retainAll(list2) );//判断list集合是否包含list2集合中的所有元素 System.out.println( list.removeAll(list2) );//删除list集合中list2集合中的所有元素 System.out.println( list);
for循环,
方式一:因为List集合是有序的,元素有下标,所以可以根据下标进行遍历
从何开始:0 到哪结束:list.size() 如何变化:i++
for(int i = 0 ; i< list.size() ; i++) {
//根据对应的下标来获取集合对应位置上的元素
String s = list.get(i);
System.out.println(s);
}
增强for循环/高效for循环/foreach循环
方式二:普通for循环遍历效率低,可以通过foreach提高遍历效率
好处:语法简洁效率高 坏处:不能按照下标来处理数据
格式:for(1 2 : 3){循环体} 3是要遍历的数据 1和2是遍历得到的单个数据的类型 和 名字
for(String s : list) {//s就是本次循环/遍历得到的集合中的元素
System.out.println(s);
}
iterator迭代器循环
方式三:iterator() 是继承自父接口Collection的
1.获取当前集合的迭代器
Iterator it = list.iterator();
由于不清楚要遍历的集合中有多少元素,所以我们使用的循环结构是While
while(it.hasNext()) {//判断集合中是否有下个元素,如果有,返回true,继续遍历
String s = it.next();//获取对应的元素
System.out.println(s);
}
listIterator()是List接口独有的
listIterator()是List接口特有的
listIterator是Iterator的子接口,可以拥有父接口的方法,还可以有自己独有的方法(逆序遍历)
ListIterator it2 = list.listIterator();while (it2.hasNext()){
System.out.println(it2.next());
}
- 存在java.util包中
- 内部是用数组结构存放数据,封装数组的操作,每个对象都有下标 (面试题)
- 内部数组默认的初始容量是10,如果不够会以1.5倍的容量增长
- 查询快,增删数据效率会低
list.remove(300);--这样写会报错
数组下标越界,index=300,size=4
这个方法是根据下标值来删除元素的,而本集合没有300的下标,所以数组下标越界
如果想根据具体的元素女人移出元素,需要先把int类型的数据转成Integer数据类型
System.out.println(Integer.valueOf(300));
从根上讲清楚:
1.方法怎么调用的. 通过名字和参数列表
它们两个名字一样,所以看参数,
2.你传过来的300是个什么类型的参数,
如果直接传是int类型调用上面这个方法,那么他指定的位置根据下标来删除.那么你想把看做是
list当中存的元素,你定义list的时候泛型是,你在list集合当中存的300是Integer类型,
这时候才能看成元素,所以你想根据元素删除,就要把int 300转换成Integer300,通过valueof
高效效果是在127~-128之间
链表,两端效率高,底层就是链表实现的
LinkedList() 构造一个空列表
void addFirst(E e) 将指定元素插入此列表的开头
void addLast(E e) 将指定元素添加到此列表的结尾
E getFirst() 返回此列表的第一个元素
E getLast() 返回此列表的最后一个元素
E removeFirst()移除并返回此列表的第一个元素
E removeLast() 移除并返回此列表的最后一个元素
E element() 获取但不移除此列表的头(第一个元素)
boolean offer(E e) 将指定元素添加到此列表的末尾(最后一个元素)
boolean offerFirst(E e) 在此列表的开头插入指定的元素
boolean offerLast(E e) 在此列表末尾插入指定的元素
E peek() 获取但不移除此列表的头(第一个元素)
E peekFirst() 获取但不移除此列表的第一个元素;如果此列表为空,则返回 null
E peekLast() 获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null
E poll()获取并移除此列表的头(第一个元素)
E pollFirst() 获取并移除此列表的第一个元素;如果此列表为空,则返回 null
E pollLast() 获取并移除此列表的最后一个元素;如果此列表为空,则返回 null
其他测试
4.1创建对象
LinkedList list2 = new LinkedList();
4.2添加数据
list2.add("水浒传");
list2.add("三国演义");
list2.add("西游记");
list2.add("红楼梦");
System.out.println(list2);
System.out.println(list2.element());//获取但不移除此列表的首元素(第一个元素)
/**别名:查询系列*/
System.out.println(list2.peek());//获取但不移除此列表的首元素(第一个元素)
System.out.println(list2.peekFirst());//获取但不移除此列表的首元素(第一个元素)
System.out.println(list2.peekLast());//获取但不移除此列表的尾元素(最后一个元素)
/**别名:新增系列*/
System.out.println(list2.offer("遮天"));//将指定元素添加到列表末尾
System.out.println(list2.offerFirst("斗罗大陆"));//将指定元素插入列表开头
System.out.println(list2.offerLast("斗破苍穹"));//将指定元素插入列表末尾
System.out.println(list2);
/**别名:移除系列*/
System.out.println(list2.poll());//获取并且移除此列表的首元素(第一个元素),成功移除,返回移除元素
System.out.println(list2.pollFirst());//获取并且移除此列表的首元素(第一个元素),成功移除,返回移除元素,如果此列表为空,则返回null
System.out.println(list2.pollLast());//获取并且移除此列表的尾元素(最后一个元素),成功移除,返回移除元素,如果此列表为空,则返回null
System.out.println(list2);
ArrayList相当于在没指定initialCapacity时就是会使用延迟分配对象数组空间,当第一次插入元素时才分配10(默认)个对象空间。假如有20个数据需要添加,那么会分别在第一次的时候,将ArrayList的容量变为10;之后扩容会按照1.5倍增长。也就是当添加第11个数据的时候,Arraylist继续扩容变为10*1.5=15;当添加第16个数据时,继续扩容变为15 * 1.5 =22个
ArrayList没有对外暴露其容量个数,查看源码我们可以知道,实际其值存放在elementData对象数组中,那我们只需拿到这个数组的长度,观察其值变化了几次就知道其扩容了多少次。怎么获取呢?只能用反射技术了。
- Set是一个不包含重复数据的Collection
- Set集合中的数据是无序的(因为Set集合没有下标)
- Set集合中的元素不可以重复 – 常用来给数据去重
- 数据无序且数据不允许重复
- HashSet : 底层是哈希表,包装了HashMap,相当于向HashSet中存入数据时,会把数据作为K,存入内部的HashMap中。当然K仍然不许重复。
- TreeSet : 底层是TreeMap,也是红黑树的形式,便于查找数据
学习Collection接口中的方法即可
底层是哈希表,包装了HashMap,相当于向HashSet中存入数据时,会把数据作为K存入内部的HashMap中,其中K不允许重复,允许使用null.
Java.util接口Map
类型参数 : K - 表示此映射所维护的键 V – 表示此映射所维护的对应的值
也叫做哈希表、散列表. 常用于键值对结构的数据.其中键不能重复,值可以重复
- Map可以根据键来提取对应的值
- Map的键不允许重复,如果重复,对应的值会被覆盖
- Map存放的都是无序的数据
- Map的初始容量是16,默认的加载因子是0.75
TIPS:源码摘抄:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
初始容量1<<4,相当于1*(2^4),也就是16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认的加载因子是0.75f,也就是存到75%开始扩容,按照2的次幂进行扩容
学习Collection接口中的方法即可
void clear() 从此映射中移除所有映射关系(可选操作) boolean containsKey(Object key) 如果此映射包含指定键的映射关系,则返回 true
boolean containsValue(Object value) 如果此映射将一个或多个键映射到指定值,则返回 true
Set> entrySet() 返回此映射中包含的映射关系的 Set 视图
boolean equals(Object o) 比较指定的对象与此映射是否相等
V get(Object key) 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null
int hashCode() 返回此映射的哈希码值
boolean isEmpty() 如果此映射未包含键-值映射关系,则返回 true
Set keySet() 返回此映射中包含的键的 Set 视图
V put(K key, V value) 将指定的值与此映射中的指定键关联(可选操作)
void putAll(Map extends K,? extends V> m)从指定映射中将所有映射关系复制到此映射中(可选操作)
V remove(Object key) 如果存在一个键的映射关系,则将其从此映射中移除(可选操作)
int size() 返回此映射中的键-值映射关系数
Collection values() 返回此映射中包含的值的 Collection 视图
public static void main(String[] args) {
//1.创建Map对象
/*Map中的数据要符合映射规则,一定注意要同时指定 K 和 V 的数据类型
* 至于这个K和V要指定成什么类型的数据,取决于你的具体需求 */
Map map = new HashMap<>();//注意导包:java.util//2.常用方法测试
map.put(9527,"白骨精");//向集合中添加数据
map.put(9528,"黑熊精");
map.put(9528,"者行孙");
map.put(9529,"黄毛怪");
System.out.println(map);//查看集合中的元素
/*总结1:Map中存放的都是无序的数据
总结2 :Map中的key不可以重复,如果重复,此Key对应的Value会被覆盖
打印结果:{9527=白骨精, 9528=者行孙, 9529=黄毛怪},没有黑熊精*/
//3.测试常用方法
//map.clear();//清空集合
System.out.println(map.containsKey(9528));//true,查看是否包含指定的Key
System.out.println(map.containsValue("土地老儿"));//false,查看是否包含指定的Value
System.out.println(map.equals("者行孙"));//false,判断"者行孙"与map对象是否相等
System.out.println(map.get(9528));//者行孙,根据指定的Key来获取对应的Value
System.out.println(map.hashCode());//102939160,获取哈希码值
System.out.println(map.isEmpty());//false,判断集合是否为空
System.out.println(map.remove(9528));//删除指定的元素
System.out.println(map.get(9528));//null,说明映射关系已被移出
System.out.println(map.size());//2,获取集合中元素的个数
Collection values = map.values();//把map集合中的所有Vlaue收集起来放到Collection中
System.out.println(values);//[白骨精, 黄毛怪]/*对map集合进行遍历/迭代*/
/*方式一
* 遍历map中的数据,但是map本身没有迭代器,所以需要转换成set集合
* Set :把map集合中的所有Key存到set集合中--keySet()*/
//1.将map中的key取出存入set集合,集合的泛型就是key的类型Integer
Set keySet = map.keySet();
//2.想要遍历集合,先获取集合的迭代器对象
Iterator it = keySet.iterator();
//3.循环遍历集合中的所有元素
while(it.hasNext()){//判断是否有下个元素
Integer key = it.next();//拿到当前遍历到的key
String value = map.get(key);//通过刚刚拿到的key获取对应的value
System.out.println("{"+key+"="+value+"}");
}
/*方式二
遍历map数据,把map转成set集合,是把map中的一对Key&Value作为一个Entry整体放入Set
一对 K V 是一个 Entry */
//1.将map集合的每一对键值对作为Entry,逐一放入Set集合
//所以set存的是Entry对象,Entry的类型是Entry
Set> entrySet = map.entrySet();
//2.获取set集合的迭代器Iterator>
Iterator> it2 = entrySet.iterator();
//3.循环遍历set集合中的元素
while (it2.hasNext()){//如果有元素就继续遍历
//4.获取当前集合遍历到的entry对象
Map.Entry entry = it2.next();
Integer key = entry.getKey();//5.获取当前entry中存着的key
String value = entry.getValue();//6.获取当前entry中存着的value
System.out.println("{"+key+"="+value+"}");//7.拼接打印输出结果
HashMap的键要同时重写hashCode()和equlas()
hashCode()用来判定二者的hash值是否相同,重写后根据属性生成
equlas()用来判断属性的值是否相同,重写后,根据属性判断
–equlas()判断数据如果相等,hashCode()必须相同
–equlas()判断数据如果不等,hashCode()尽量不同
HashMap底层是一个Entry[ ]数组,当存放数据时,会根据hash算法来计算数据的存放位置
算法:hash(key)%n , n就是数组的长度,其实也就是集合的容量
当计算的位置没有数据的时候,会直接存放数据
当计算的位置,有数据时,会发生hash冲突/hash碰撞,解决的办法就是采用链表的结构,在数组中指定位置处已有元素之后插入新的元素,也就是说数组中的元素都是最早加入的节点
HashMap是什么结构:
数组加链表.
高级回答:数组加链表.但是链表长度大于8时,会转换成红黑树, 小于6时,会退回成链表.
/*本类用来完成Map集合相关练习 * 需求:提示并接收用户输入的一串字符,并统计出每个字符出现的次数*/ public class TestMap2 { public static void main(String[] args) { //1.提示用户输入要统计的字符串 System.out.println("请输入您要统计的字符串:"); //2.接收用户输入的字符串 String input = new Scanner(System.in).nextLine(); //3.创建map集合来保存数据,打印效果{a=2,f=5,h=1} /*统计的是每个字符出现的次数,所以字符是char类型,次数是int,需要使用包装类型*/ Map
map = new HashMap<>(); //4.使用循环结构遍历用户输入的每一个字符 for(int i = 0;i < input.length();i++){ char key = input.charAt(i);//获取字符串上指定位置处的字符 System.out.println("第"+i+"轮获取到的字符是:"+key); //5.统计每个字符出现的次数,存起来,存到map //5.1先拿着获取到的字符(key)查看一下是否有value Integer value = map.get(key); if(value == null){//如果判断为null,说明之前没有出现过这个字符 map.put(key,1);//设置次数为1 }else{//如果不是null,说明之前存过值,给之前的次数+1 map.put(key,value+1); } } System.out.println("各个字符出现的频率是:"); System.out.println(map); } }
成长因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
前面的讲述已经发现,当你空间只有仅仅为10的时候是很容易造成2个对象的hashcode 所对应的地址是一个位置的情况。这样就造成 2个 对象会形成散列桶(链表)。这时就有一个加载因子的参数,值默认为0.75 ,如果你hashmap的 空间有 100那么当你插入了75个元素的时候 hashmap就需要扩容了,不然的话会形成很长的散列桶结构,对于查询和插入都会增加时间,因为它要一个一个的equals比较。但又不能让加载因子很小,如0.01,这样显然是不合适的,频繁扩容会大大消耗你的内存。这时就存在着一个平衡,jdk中默认是0.75,当然负载因子可以根据自己的实际情况进行调整。
进程就是正在运行的程序,它代表了程序所占用的内存区域
独立性
进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
动态性
进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的.
并发性
多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响.
HA(High Availability)高可用:指在高并发的情景中,尽可能的保证程序的可用性,减少系统不能提供服务的时间
线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.
一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程
我们看到的进程的切换,切换的也是不同进程的主线程
多线程扩展了多进程的概念,使的同一个进程可以同时并发处理多个任务
一个操作系统中可以有多个进程,一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)
每个线程在共享同一个进程中的内存的同时,又有自己独立的内存空间.
所以想使用线程技术,得先有进程,进程的创建是OS操作系统来创建的,一般都是C或者C++完成
线程的随机性指的是同一时刻,只有一个程序在执行
我们宏观上觉得这些程序像是同时运行,但是实际上微观时间是因为CPU在高效的切换着,这使得各个程序从表面上看是同时进行的,也就是说,宏观层面上,所有的进程/线程看似同时运行,但是微观层面上,同一时刻,一个CPU只能处理一件事.切换的速度甚至是纳秒级别的,非常快
时间片,即CPU分配给各个线程的一个时间段,称作它的时间片,即该线程被允许运行的时间,如果在时间片用完时线程还在执行,那CPU将被剥夺并分配给另一个线程,将当前线程挂起,如果线程在时间片用完之前阻塞或结束,则CPU当即进行切换,从而避免CPU资源浪费,当再次切换到之前挂起的线程,恢复现场,继续执行。
注意:我们无法控制OS选择执行哪些线程,OS底层有自己规则,如:FCFS(First Come First Service 先来先服务算法)
SJS(Short Job Service短服务算法)
由于线程状态比较复杂,我们由易到难,先学习线程的三种基础状态及其转换,简称”三态模型” :
就绪(可运行)状态:线程已经准备好运行,只要获得CPU,就可立即执行
执行(运行)状态:线程已经获得CPU,其程序正在运行的状态
阻塞状态:正在运行的线程由于某些事件(I/O请求等)暂时无法执行的状态,即线程执行阻塞
就绪 → 执行:为就绪线程分配CPU即可变为执行状态"
执行 → 就绪:正在执行的线程由于时间片用完被剥夺CPU暂停执行,就变为就绪状态
执行 → 阻塞:由于发生某事件,使正在执行的线程受阻,无法执行,则由执行变为阻塞
(例如线程正在访问临界资源,而资源正在被其他线程访问)
反之,如果获得了之前需要的资源,则由阻塞变为就绪状态,等待分配CPU再次执行
我们可以再添加两种状态:
- 创建状态:线程的创建比较复杂,需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中
- 终止状态:等待OS进行善后处理,最后将PCB清零,并将PCB返回给系统
PCB(Process Control Block):为了保证参与并发执行的每个线程都能独立运行,OS配置了特有的数据结构PCB来描述线程的基本情况和活动过程,进而控制和管理线程
新建状态(New) : 当线程对象创建后就进入了新建状态.如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法,线程即为进入就绪状态.
处于就绪(可运行)状态的线程,只是说明线程已经做好准备,随时等待CPU调度执行,并不是执行了t.start()此线程立即就会执行
运行状态(Running):当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态
就绪状态是进入运行状态的唯一入口,也就是线程想要进入运行状态状态执行,先得处于就绪状态
阻塞状态(Blocked):处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行.
根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:
等待阻塞:运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态
同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态
其他阻塞:调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例
启动线程的唯一方法就是通过Thread类的start()实例方法
start()方法是一native方法,它将通知底层操作系统,.最终由操作系统启动一个新线程,操作系统将执行run()
这种方式实现的多线程很简单,通过自己的类直接extends Thread,并重写run()方法,就可以自动启动新线程并执行自己定义的run()方法
模拟开启多个线程,每个线程调用run()方法.注意:实现业务,必须放在重写run里.
构造方法:
Thread() 分配新的Thread对象
Thread(String name) 分配新的Thread对象
Thread(Runnable target) 分配新的Thread对象
Thread(Runnable target,String name) 分配新的Thread对象
普通方法:
static Thread currentThread( )
返回对当前正在执行的线程对象的引用
long getId()
返回该线程的标识
String getName()
返回该线程的名称
void run()
如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法
static void sleep(long millions)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
void start()
使该线程开始执行:Java虚拟机调用该线程的run()
public static void main(String[] args) { /*如果只是调用两个线程的run(),那么会先执行完一个线程,再执行另一个线程 * 不会有多线程抢占资源的效果,所以,我们真的能通过run()来执行多线程任务吗?*/ /*run()与start()本质上的区别,run()只能当做一个顺序执行的单线程普通方法 * 并没有多线程抢占的效果,所以,如果想以多线程的效果干活,必须调用start() * 才能真正的起动线程*/ //5.创建自定义线程类对象 MyThread t = new MyThread();/*new 对应的就是新建状态*/ //6.模拟多线程,需要至少启动2个线程,如果只启动一个线程,是单线程程序 MyThread t2 = new MyThread(); MyThread t3 = new MyThread("旺财"); //t2.run(); //t.run(); /*当我们调用start()启动线程时,虚拟机会自动调用run()的业务*/ t.start();/*对应的是就绪状态*/ t2.start(); t3.start(); /*测试结果: * 线程有随机性,执行的结果是不可控的.因为这是CPU在调度的,我们无法控制 * */ } }
自定义多线程类:
/**1.方式1:extends Thread*/
class MyThread extends Thread{
/**最后:为了修改线程名称,不再使用系统分配的默认名称,需要提供含参构造*/
//右键-->Source-->倒数第二个-->DisSelect All-->选择无参构造或者传名字的构造
public MyThread() {
super();
}public MyThread(String name) {
super(name);
}
//2.1线程中的业务必须写在run()里
/**2.源码:745行*/
// @Override
// public void run() {
// if (target != null) {
// target.run();
// }
// }
/**3.如果不满意run()的内容,可以重写alt+/*/
//2.2 重写Thread父类中的run()
@Override
public void run() {
/**4.super表示父类对象的引用,也就是默认使用Thread类里的业务,不用*/
//super.run();
//3.写业务:输出10次当前正在执行的线程名称
for (int i = 0; i < 10; i++) {
/**5.getName()可以获取正在执行任务的线程名称,是从父类中继承的方法,可以直接使用*/
System.out.println(i+"="+getName());
}
}
}
如果自己的类已经extends另一个类,就无法多继承,此时,可以实现一个Runnable接口
void run()使用实现接口Runnable的对象创建线程时,启动该线程将导致在独立执行的线程中调用对象的run()方法
- 继承Thread类
优点: 编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程
缺点: 自定义的线程类已继承了Thread类,所以后续无法再继承其他的类
实现Runnable接口
优点: 自定义的线程类只是实现了Runnable接口或Callable接口,后续还可以继承其他类,在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码、还有数据分开(解耦),形成清晰的模型,较好地体现了面向对象的思想
缺点: 编程稍微复杂,如想访问当前线程,则需使用Thread.currentThread()方法
MyRunnable target = new MyRunnable(); Thread t1 = new Thread(target); Thread t2 = new Thread(target); Thread t3 = new Thread(target); t1.start(); t2.start(); t3.start();
把target作为参数传过start()的构造函数
每次创建线程对象,都会生成一个tickets变量值是100,创建4次对象就生成了400张票了。不符合需求,怎么解决呢?能不能把tickets变量在每个对象间共享,就保证多少个对象都是卖这100张票。
解决方案: 用静态修饰
产生超卖,0 张 、-1张、-2张。
产生重卖,同一张票卖给多人。
多线程安全问题是如何出现的?常见情况是由于线程的随机性+访问延迟。
以后如何判断程序有没有线程安全问题?
在多线程程序中 + 有共享数据 + 多条语句操作共享数据
经过前面多线程编程的学习,我们遇到了线程安全的相关问题,比如多线程售票情景下的超卖/重卖现象.
在多线程程序中 + 有共享数据 + 多条语句操作共享数据
多线程的场景和共享数据的条件是改变不了的(就像4个窗口一起卖100张票,这个是业务)
所以思路可以从第3点"多条语句操作共享数据"入手,既然是在这多条语句操作数据过程中出现了问题
那我们可以把有可能出现问题的代码都包裹起来,一次只让一个线程来执行
那怎么"把有可能出现问题的代码都包裹起来"呢?我们可以使用synchronized关键字来实现同步效果
也就是说,当多个对象操作共享数据时,可以使用同步锁解决线程安全问题,被锁住的代码就是同步的接下来介绍下同步与异步的概念:
同步:体现了排队的效果,同一时刻只能有一个线程独占资源,其他没有权利的线程排队。
坏处就是效率会降低,不过保证了安全。
异步:体现了多线程抢占资源的效果,线程间互相不等待,互相抢占资源。
坏处就是有安全隐患,效率要高一些。
synchronized (锁对象){
需要同步的代码(也就是可能出现问题的操作共享数据的多条语句);
}
同步效果的使用有两个前提:
- 前提1:同步需要两个或者两个以上的线程(单线程无需考虑多线程安全问题)
- 前提2:多个线程间必须使用同一个锁(我上锁后其他人也能看到这个锁,不然我的锁锁不住其他人,就没有了上锁的效果)
synchronized同步关键字可以用来修饰方法,称为同步方法,使用的锁对象是this
synchronized同步关键字可以用来修饰代码块,称为同步代码块,使用的锁对象可以任意
同步的缺点是会降低程序的执行效率,但我们为了保证线程的安全,有些性能是必须要牺牲的
但是为了性能,加锁的范围需要控制好,比如我们不需要给整个商场加锁,试衣间加锁就可以了
为什么同步代码块的锁对象可以是任意的同一个对象,但是同步方法使用的是this呢?
因为同步代码块可以保证同一个时刻只有一个线程进入
但同步方法不可以保证同一时刻只能有一个线程调用,所以使用本类代指对象this来确保同步
同步锁:相当于给容易出现问题的代码加了一把锁,包裹了所有可能会出现安全隐患的代码 加锁之后,就有同步(排队)的效果,但是加锁的范围,需要考虑: 不能太大,太大,干啥都得排队,效率低,范围太小,太小,锁不住,还是会有安全隐患
同步代码块:sychronized(锁对象){会出现安全问题的代码}
同步代码块中,同一时刻,同一资源,只能被一个线程独享
创建一个唯一的锁对象
不论之后是哪个线程进同步代码块,使用的都是唯一的锁对象,"唯一"很重要
Object o = new Object();
修改同步代码块的锁对象为成员变量o,注意,"唯一"很重要
如果一个方法中的所有代码都需要同步,那这个方法可以设置成同步方法
注意:如果是继承的方式的话,锁对象最好用"类名.class",否则创建自定义线程类多个对象时,无法保证锁的唯一
加锁,
1.把涉及到的共享数据操作语句都包起来,
2.锁对象 Object o = new Object();
注意:如果是继承的方式的话,锁对象最好用"类名.class",否则创建自定义线程类多个对象时,无法保证锁的唯一
StringBuffer JDK1.0
加了synchronized ,性能相对较低(要排队,同步),安全性高
StringBuilder JDK1.5
去掉了synchronized,性能更高(不排队,异步),存在安全隐患
ExecutorService:用来存储线程的池子,把新建线程/启动线程/关闭线程的任务都交给池来管理
- execute(Runnable任务对象) 把任务丢到线程池
Executors 辅助创建线程池的工具类
- newFixedThreadPool(int nThreads) 最多n个线程的线程池
- newCachedThreadPool() 足够多的线程,使任务不必等待
- newSingleThreadExecutor() 只有一个线程的线程池
线程池ExecutorService:用来存储线程的池子,把新建线程/启动线程/关闭线程的任务都交给池来管理
Executors用来辅助创建线程池对象,newFixedThreadPool()创建具有参数个数的线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
for(int i = 0;i<5;i++) {
excute()让线程池中的线程来执行任务,每次调用都会启动一个线程
pool.execute(target);//本方法的参数就是执行的业务,也就是实现类的目标对象
悲观锁:像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态.
悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。乐观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态.
乐观锁认为竞争不总是会发生,因此它不需要持有锁,将”比较-替换”这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
synchronized 互斥锁(悲观锁,有罪假设)
采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。
每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。ReentrantLock 排他锁(悲观锁,有罪假设)
ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,实际上独占锁是一种相对比较保守的锁策略,在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。然而读操作之间不存在数据竞争问题,如果”读/读”操作能够以共享锁的方式进行,那会进一步提升性能。ReentrantReadWriteLock 读写锁(乐观锁,无罪假设)
因此引入了ReentrantReadWriteLock,顾名思义,ReentrantReadWriteLock是Reentrant(可重入)Read(读)Write(写)Lock(锁),我们下面称它为读写锁。
读写锁内部又分为读锁和写锁,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景。
package cn.tedu.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 本类用于改造售票案例,使用可重入读写锁
* ReentrantReadWriteLock
* */
public class TestSaleTicketsV3 {
public static void main(String[] args) {
SaleTicketsV3 target = new SaleTicketsV3();
Thread t1 = new Thread(target);
Thread t2 = new Thread(target);
Thread t3 = new Thread(target);
Thread t4 = new Thread(target);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class SaleTicketsV3 implements Runnable{
static int tickets = 100;
//1.定义可重入读写锁对象,静态保证全局唯一
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
@Override
public void run() {
while(true) {
//2.在操作共享资源前上锁
lock.writeLock().lock();
try {
if(tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "=" + tickets--);
}
if(tickets <= 0) break;
} catch (Exception e) {
e.printStackTrace();
}finally {
//3.finally{}中释放锁,注意一定要手动释放,防止死锁,否则就独占报错了
lock.writeLock().unlock();
}
}
}
}
需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁会自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!
与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)从理论上讲,与互斥锁定相比,使用读-写锁允许的并发性增强将带来更大的性能提高。
注解很厉害,它可以增强我们的java代码,同时利用反射技术可以扩充实现很多功能。它们被广泛应用于三大框架底层。
传统我们通过xml文本文件声明方式(如下图,但是XML比较繁琐且不易检查),而现在最主流的开发都是基于注解方式,代码量少,框架可以根据注解去自动生成很多代码,从而减少代码量,程序更易读。例如最火爆的SpringBoot就完全基于注解技术实现\
注解设计非常精巧,初学时觉得很另类甚至多余,甚至垃圾。有了java代码干嘛还要有@注解呢?但熟练之后你会赞叹,它竟然可以超越java代码的功能,让java代码瞬间变得强大。大家慢慢体会吧。
- JDK自带注解
- 元注解
- 自定义注解
@Override :用来标识重写方法
@Deprecated标记就表明这个方法已经过时了,但我就要用,别提示我过期
@SuppressWarnings(“deprecation”) 忽略警告
@SafeVarargs jdk1.7出现,堆污染,不常用
@FunctionallInterface jdk1.8出现,配合函数式编程拉姆达表达式,不常用
用来描述注解的注解,就5个:
@Target 注解用在哪里:类上、方法上、属性上等等
@Retention 注解的生命周期:源文件中、字节码文件中、运行中
@Inherited 允许子注解继承
@Documented 生成javadoc时会包含注解,不常用
@Repeatable注解为可重复类型注解,可以在同一个地方多次使用,不常用
描述注解存在的位置:
ElementType.TYPE 应用于类的元素
ElementType.METHOD 应用于方法级
ElementType.FIELD 应用于字段或属性(成员变量)
ElementType.ANNOTATION_TYPE 应用于注释类型
ElementType.CONSTRUCTOR 应用于构造函数
ElementType.LOCAL_VARIABLE 应用于局部变量
ElementType.PACKAGE 应用于包声明
ElementType.PARAMETER 应用于方法的参数
该注解定义了自定义注解被保留的时间长短,比如某些注解仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中; 编译在class文件中的注解可能会被虚拟机忽略,而另一些在class被装载时将被读取。
SOURCE 在源文件中有效(即源文件保留)
CLASS 在class文件中有效(即class保留)
RUNTIME 在运行时有效(即运行时保留)
注意:注解的语法写法和常规java的语法写法不同
/*本类用于测试自定义注解*/ public class TestAnnotation { } /*1.2 通过@Target注解表示此自定义注解可以使用的位置 * 注意:@Target注解使用时需要导包,我们通过"ElementType.静态常量值"的方式来指定 * 此自定义注解可以使用的位置 * 如果有多个值,可以使用"{ , }"的格式来写:@Target({ElementType.METHOD,ElementType.TYPE})*/ @Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD})//表示此注解可以加在方法上 /*1.3通过@Retention注解表示自定义注解的生命周期 * 注意:@Retention使用时也需要导包,通过RetentionPolicy.静态常量值来指定此自定义注解的生命周期 * 也就是指自定义注解可以存在在哪个文件中:源文件中/字节码文件中/运行时有效 * 而且这三个值,只能3选1,不能同时写多个*/ @Retention(RetentionPolicy.SOURCE) /*0.首先注意:自定义注解的语法与java不同,不要套用java的格式*/ //1.定义自定义主解,注解名是Test,并通过两个元注解表示此注解的作用位置和生命周期 /*1.注解定义要使用"@interface 注解名"的方式来定义*/ @interface Test{ /*3.自定义注解还可以添加功能--给注解添加属性 * 注意 int age();不是方法的定义,而是给自定义注解中定义age属性的语法 * 如果为了使用注解时方便,还可以给属性指定默认值,这样就可以直接使用,格式:int age() default 0; * */ //int age(); //给注解定义一个普通的属性age int age() default 0; /*4.注解中还可以添加功能-可以定义特殊属性value * 特殊属性的定义方式与别的属性相同,主要是使用方式不同 * 使用此注解给属性赋值的时候,可以不用写成"@Test(value = "Apple")", * 格式可以简化"@Test("Apple")"直接写值 * 但是自定义注解类中的赋予默认值不可以简写,如果自定义了默认值,可以不用赋值,直接使用*/ String value() default "apple"; } /*2.使用注解时,只要在指定的自定义注解名字前加上"@"即可使用此注解*/ //2.定义一个类用来测试自定义注解 //@Test class TeatAnno{ /*测试1:分别给TestAnno类/name属性/eat()都添加了@Test注解,只有方法上不报错 * 结论:自定义注解能够加在什么位置,取决于@Target注解的值 * 测试2:修改@Target注解的值:@Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD}) * 结论:注解@Test可以存在于多个位置,如果@Target有多个值,格式是"{ , }" * 原因:Target注解的源码:ElementType[] value();*/ //@Test String name; /*测试3:当我们添加了注解的age属性时,@Test注解报错 * 结论:当注解没有定义属性时,可以直接使用,如果有属性了,就需要给属性赋值 * 测试4:给@Test注解的age属性赋值以后,就不报错了 * 结论:给属性赋值的格式" @Test(age = 10)",注意,不能直接写10,这是错误格式 * 测试5:给age属性赋予默认值后,可以不加属性值,直接使用注解,此时使用的就是age的默认值 * */ /*测试6:给特殊属性value赋值时可以简写,想当于value="apple"*/ @Test/*测试7:因为已有默认值,所以不用给特殊属性赋值,直接使用@Test*/ //@Test(age = 10) public void eat(){ System.out.println("又到干饭时间啦~"); }
代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
JAVA一共有23种设计模式,我们今天首先来学其中一种:单例设计模式
单例模式可以说是大多数开发人员在实际中使用最多的,常见的Spring默认创建的bean就是单例模式的。
单例模式有很多好处,比如可节约系统内存空间,控制资源的使用。
其中单例模式最重要的是确保对象只有一个。
简单来说,保证一个类在内存中的对象就一个。
RunTime就是典型的单例设计,我们通过对RunTime类的分析,一窥究竟。
- 对本类构造方法私有化,防止外部调用构造方法创建对象
- 创建全局唯一的对象,也做私有化处理
- 通过自定义的公共方法将创建好的对象返回(类似封装属性后的getXxx() )
思考:构造方法和对象私有化后,通过公共的访问点来获取对象,那外界如何调用这个公共方法呢?
之前我们都是在外部创建本类对象并进行方法调用,但是现在单例程序中外部无法直接创建本类对象
解决方案:我们可以利用之前学习的静态的概念,将方法修饰成静态的,就可以通过类名直接调用啦
注意事项:静态只能调用静态,所以静态方法中返回的对象也需用静态修饰
public static void main(String[] args) { 通过类名调用getSingle()方法获取本类对象 MySingle my = MySingle.getSingle(); MySingle my1 = MySingle.getSingle(); 测试获取到的这两个引用类型变量是否相等 System.out.println(my == my1);true,==比较的是地址值,说明是同一个对象 } } //创建自己的单例程序 class MySingle{ 提供构造方法,并将构造方法私有化 构造方法私有化的目的:为了不让外界随意实例化/new本类对象 private MySingle(){ }; 在类的内部,创建本类对象,并且私有化 本资源也需要使用static修饰,因为静态方法getSingle()只能调用静态资源 static private MySingle single = new MySingle(); 也就是以公共的方式向外界提供获取本类私有对象的方法 对外提供一个公共的全局访问点 用static关键字来修饰本方法,为了外部可以通过类名直接调用本方法 static public MySingle getSingle(){ 把内部创建好的对象返回到调用位置,谁调用这个方法,谁就能拿到返回的single对象 return single; } }
思考:构造方法和对象私有化后,通过公共的访问点来获取对象,那外界如何调用这个公共方法呢?
之前我们都是在外部创建本类对象并进行方法调用,但是现在单例程序中外部无法直接创建本类对象
解决方案:我们可以利用之前学习的静态的概念,将方法修饰成静态的,就可以通过类名直接调用啦
注意事项:静态只能调用静态,所以静态方法中返回的对象也需用静态修饰
/*本类用于测试单例设计模式 2 - 懒汉式--面试重点!!!*/ /*总结: * 关于单例设计模式的两种实现方式: * 1.饿汉式 : 不管你用不用这个类的对象,都会直接先创建一个 * 2.懒汉式 : 先不给你创建这个类的对象,等你需要的时候再帮你创建--利用了延迟加载的思想 * 延迟加载的思想:是指不会在第一时间就把对象创建好来占用内存,而是什么时候用到,什么时候再去创建对象 * 3.线程安全问题 : 是指共享资源有线程并发的数据安全隐患,可以通过加锁的方式[同步代码块/同步方法] * */ public class Singleton2 { public static void main(String[] args) { //6.创建对象进行测试 MySingle2 s1 = MySingle2.getMySingle2(); MySingle2 s2 = MySingle2.getMySingle2(); System.out.println( s1 == s2 );//true,比较的是地址值,说明是同一个对象 System.out.println( s1 );//cn.tedu.single.MySingle2@1b6d3586 System.out.println( s2 );//cn.tedu.single.MySingle2@1b6d3586 } } //0.创建单例程序 class MySingle2{ //1.私有化构造方法,为了防止外部调用构造方法直接创建本类对象 private MySingle2(){} //2.在类的内部创建好引用类型变量(延迟加载的思想)--注意私有化 //5.2本处的引用类型变量也需要修饰成static的,因为静态只能调用静态,getMySingle2()是静态方法 static private MySingle2 single2; //7.2.2同步代码块中使用的唯一的锁对象 static Object o = new Object(); /*问题:程序中有共享资源single2,并且有多条语句(3句)操作了共享资源 * 此时single2共享资源在多线程环境下一定会存在多线程数据安全隐患 * 解决方案1:同步代码块[加锁,范围是操作共享资源的所有代码] * 解决方案2:同步方法[如果方法中的所有代码都需要被同步,那么这个方法可以修饰成同步方法] * 注意事项:锁对象在静态方法中,不可以使用this,因为静态资源优先于对象加载 * 锁对象可以使用外部创建好的唯一的锁对象o,但请注意,需要是静态的,静态只能调用静态 * */ //3.对外提供公共的全局访问点 //5.1注意要使用static来修饰本公共方法,为了方便后续可以通过类名直接调用 //7.1将方法修饰成同步方法 synchronized static public MySingle2 getMySingle2(){ //4.当用户调用此方法时,才说明用到这个对象了,那么我们就把这个对象返回 /*注意:这里需要增加一个判断 如果调用方法时single2的值为null,说明之前没有new过,保存的是默认值 这时才需要new对象,如果single2的值不为null,直接return single2即可*/ //7.2可以将操作共享资源的多条语句放入同步代码块之中 synchronized (o) { //synchronized (this) { if (single2 == null) { single2 = new MySingle2();//没有对象时才创建对象,并赋值给single2 } return single2;//有对象则直接返回single2 } }
问题:
程序中有共享资源single2,并且有多条语句(3句)操作了共享资源 * 此时single2共享资源在多线程环境下一定会存在多线程数据安全隐患
解决方案1:同步代码块[加锁,范围是操作共享资源的所有代码]
解决方案2:同步方法[如果方法中的所有代码都需要被同步,那么这个方法可以修饰成同步方法]
注意事项:锁对象在静态方法中,不可以使用this,因为静态资源优先于对象加载
锁对象可以使用外部创建好的唯一的锁对象o,但请注意,需要是静态的,静态只能调用静态
总结: 关于单例设计模式的两种实现方式: 1.饿汉式 : 不管你用不用这个类的对象,都会直接先创建一个 2.懒汉式 : 先不给你创建这个类的对象,等你需要的时候再帮你创建--利用了延迟加载的思想 延迟加载的思想:是指不会在第一时间就把对象创建好来占用内存,而是什么时候用到,什么时候再去创建对象 3.线程安全问题 : 是指共享资源有线程并发的数据安全隐患,可以通过加锁的方式[同步代码块/同步方法]
Reflection(反射) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查,或者说“自审”,也有称作“自省”。
反射非常强大,它甚至能直接操作程序的私有属性。我们前面学习都有一个概念,被private封装的资源只能类内部访问,外部是不行的,但这个规定被反射赤裸裸的打破了。
反射就像一面镜子,它可以在运行时获取一个类的所有信息,可以获取到任何定义的信息(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。
如果想创建对象,我们直接new User(); 不是很方便嘛,为什么要去通过反射创建对象呢?
那我要先问你个问题了,你为什么要去餐馆吃饭呢?
例如:我们要吃个牛排大餐,如果我们自己创建,就什么都得管理。
好处是,每一步做什么我都很清晰,坏处是什么都得自己实现,那不是累死了。牛接生你管,吃什么你管,屠宰你管,运输你管,冷藏你管,烹饪你管,上桌你管。就拿做菜来说,你能有特级厨师做的好?
那怎么办呢?有句话说的好,专业的事情交给专业的人做,饲养交给农场主,屠宰交给刽子手,烹饪交给特级厨师。那我们干嘛呢?
我们翘起二郎腿直接拿过来吃就好了。
再者,饭店把东西做好,不能扔到地上,我们去捡着吃吧,那不是都成原始人了。那怎么办呢?很简单,把做好的东西放在一个容器中吧,如把牛排放在盘子里。我们在后面的学习中,会学习框架,有一个框架Spring就是一个非常专业且功能强大的产品,它可以帮我们创建对象,管理对象。以后我无需手动new对象,直接从Spring提供的容器中的Beans获取即可。Beans底层其实就是一个Map,最终通过getBean(“user”)来获取。而这其中最核心的实现就是利用反射技术。
总结一句,类不是你创建的,是你同事或者直接是第三方公司,此时你要或得这个类的底层功能调用,就需要反射技术实现。有点抽象,别着急,我们做个案例,你就立马清晰。
Class.forName(“类的全路径”);
类名.class
对象.getClass();
获取包名 类名
clazz.getPackage().getName()//包名
clazz.getSimpleName()//类名
clazz.getName()//完整类名获取成员变量定义信息
getFields()//获取所有公开的成员变量,包括继承变量
getDeclaredFields()//获取本类定义的成员变量,包括私有,但不包括继承的变量
getField(变量名)
getDeclaredField(变量名)获取构造方法定义信息
getConstructor(参数类型列表)//获取公开的构造方法
getConstructors()//获取所有的公开的构造方法
getDeclaredConstructors()//获取所有的构造方法,包括私有
getDeclaredConstructor(int.class,String.class)获取方法定义信息
getMethods()//获取所有可见的方法,包括继承的方法
getMethod(方法名,参数类型列表)
getDeclaredMethods()//获取本类定义的的方法,包括私有,不包括继承的方法
getDeclaredMethod(方法名,int.class,String.class)反射新建实例
clazz.newInstance();//执行无参构造创建对象
clazz.newInstance(666,”海绵宝宝”);//执行含参构造创建对象
clazz.getConstructor(int.class,String.class)//获取构造方法反射调用成员变量
clazz.getDeclaredField(变量名);//获取变量
clazz.setAccessible(true);//使私有成员允许访问
f.set(实例,值);//为指定实例的变量赋值,静态变量,第一参数给null
f.get(实例);//访问指定实例变量的值,静态变量,第一参数给null反射调用成员方法
Method m = Clazz.getDeclaredMethod(方法名,参数类型列表);
m.setAccessible(true);//使私有方法允许被调用
m.invoke(实例,参数数据);//让指定实例来执行该方法
单元测试方法 : 是Java测试的最小范围,使用灵活,推荐程度:5颗星
语法要求 : @Test + void + 没有参数 + public
注意:使用时需要导包:Add JUnit4 to ClassPath
@Test public void getEnd(){ Class> nme = Student.class; /*注意!!!目前成员变量的修饰符必须是public才能获取到 采用默认的修饰符反射获取不到 public String name; public int age; */ Field[] fields = nme.getFields(); for ( Field s:fields){ System.out.println(s.getName()); System.out.println(s.getType().getName()); }
单元测试1:用来测试获取指定类的字节码对象 @Test public void getClazz() throws Exception { //1.方式1 : 通过类的全路径名[包名+类名]获取对应的字节码对象 Class> student1 = Class.forName("cn.tedu.reflection.Student"); //2.方式2 : 直接类名.class获取对应的字节码对象 Class> student2 = Student.class; //3.方式3: 通过类的对象来获取,注意,此处创建的是匿名对象,匿名对象没有名字 Class> student3 = new Student().getClass(); System.out.println(student1);//打印通过反射获取到的字节码对象 System.out.println(student2.getName());//获取全路名,cn.tedu.reflection.Student System.out.println(student3.getSimpleName());//获取类名,Student System.out.println(student3.getPackage().getName());//获名,cn.tedu.reflection
单元测试2:获取构造方法 @Test public void getConstruct(){ //1.获取Student类的字节码对象 Class> clazz = Student.class; //2.通过字节码对象获取所有的公开的构造方法 Constructor>[] cs = clazz.getConstructors(); //3.使用增强for循环遍历存放所有构造函数的数组 //for(1 2 : 3){} 3 表示的是要遍历的构造函数数组 1表示当前遍历到的构造函数的类型 2参数名 for(Constructor c: cs){ //通过本轮循环遍历到的构造函数对象,获取构造函数名 System.out.println(c.getName()); //通过本轮循环遍历到的构造函数对象,获取这个构造函数的参数类型 Class[] cp = c.getParameterTypes(); System.out.println(Arrays.toString(cp)); } }
单元测试3:获取普通方法 @Test public void getFunction() throws Exception { //获取字节码对象 Class> clazz = Class.forName("cn.tedu.reflection.Srudent"); //获取指定类中的所有普通方法 Method[] ms = clazz.getMethods(); //遍历存放所有方法的数组 for (Method m : ms){ //通过当前遍历到的方法对象获取当前方法的名字 System.out.println(m.getName()); //通过当前遍历到的方法对象获取当前方法的参数类型 Class>[] pt = m.getParameterTypes(); System.out.println(Arrays.toString(pt)); } }
单元测试方法4:获取成员变量 @Test public void getFields(){ /*注意!!!目前所有成员变量的修饰符必须是public才能获取到 * 如果属性采用的是默认的修饰符,是反射不到的*/ //1.获取Class字节码对象 /* Class>中的“?”是泛型约束的通配符,类似于“*” */ Class> clazz = Student.class; //2.通过字节码对象获取所有的成员变量 Field[] fs = clazz.getFields(); //3.遍历数组,获取每个成员变量的信息 for(Field f : fs){ System.out.println(f.getName()); System.out.println(f.getType().getName()); } }
单元测试 5 :通过单元测试来反射创建对象 方式一:通过字节码对象直接调用newInstance(),触发无参构造来创建对象 方式二:先获取指定的构造函数,再通过构造函数对象调用newInstance,触发对应类型的构造函数来创建对象 @Test public void makeObject() throws Exception { //1.获取字节码对象class Class> clazz = Student.class; //2.创建对象 Object obj = clazz.newInstance();//触发的是无参构造 System.out.println(obj);//cn.tedu.reflection.Student@4ee285c6 //3.通过全参构造创建对象 //3.1 通过指定的构造函数来创建对象 /*注意参数类型的指定,是对应类型的字节码对象,不能直接光写类型*/ Constructor> c = clazz.getConstructor(String.class,int.class); //3.2触发刚刚获取到的全参构造 Object obj2 = c.newInstance("张三",18);/*此处是多态的向上造型*/ System.out.println(obj2); /*查看对象具体的属性值,或者调用方法,需要把Object强转成指定的子类对象 * 为什么要把Object转成子类类型呢?因为想要使用子类的属性或者是特有功能 * 父类是无法使用子类的特有属性和功能的,上面的obj2就是Object类型 * Object类中没有Student中的属性和特有功能 * 这种把之前看作是父类类型的子对象,重新转回成子类对象的现象,叫做向下造型 * */ Student s = (Student) obj2; System.out.println(s.name); System.out.println(s.age); s.study(); }
什么类型? 一个一个的字节码对象
Constructor> c = clazz.getConstructor(String.class,int.class);
暴力
反射本类用于测试暴力反射 /*单元测试方法的格式:@Test + public + void + 无参 */ /*1.单元测试1:暴力反射获取和设置私有属性*/ @Test public void getFields() throws Exception { //1.获取Class字节码对象 Class> clazz = Person.class; //2.获取私有属性 Field field = clazz.getDeclaredField("name"); //3.根据获取到的属性对象获取属性的类型 System.out.println(field.getType().getName());//java.lang.String System.out.println(field.getType());//class java.lang.String //4.设置属性的值 //4.1 没有对象就通过反射的方式创建对象 Object obj = clazz.newInstance(); //4.2暴力反射!!!需要设置私有可见 field.setAccessible(true); //4.3通过字段对象来给刚刚创建的对象obj设置name属性值为“派大星” //set(m,n)-- m:要给哪个对象设置属性值,n:是要设置的属性的具体值 field.set(obj,"派大星"); //4.4通过字段对象获取刚刚给obj对象设置的私有属性的值 System.out.println(field.get(obj)); }
field.set(obj,"张修雨"); 设置fidle属性值给obj对象,设置的值是张修雨, 严格来说fidle对象是我们的name属性,是obj的name属性,设置一个什么值张修雨,最初想的是obj点不是这么规定的,测试时发生了报错了,人家是私有的,我们没法用,添加私有可见,field.setAccessible(true); System.out.println(field.get(obj)); 获取obj对象的的name属性值,field代表的是name,用get方法获取的是obj对象name的值,是反着的,以前是对象点属性,反射是通过属性对象点get把你的对象传进去.
单元测试2:暴力反射获取和设置私有方法 @Test public void getFunction() throws Exception { //1.获取Class字节码对象 Class> clazz = Class.forName("tedu.reflection.Person"); //2.通过暴力反射获取私有方法 /*getDeclaredMethod(m,x,y,z...) * m:要获取的方法名 * x,y,z...可变参数,是这个方法的参数类型,但注意要加“.class” * */ Method method = clazz.getDeclaredMethod("save",int.class,String.class); //3.1没有对象就通过反射的方式创建对象 Object obj = clazz.newInstance(); //3.2 想要执行私有方法,也需要先设置私有可见 method.setAccessible(true); /*invoke(o,x,y,z...),表示通过反射技术执行方法 * o :要执行的是哪个对象的方法 * x,y,z...:执行这个方法【method对象代表的之前获取到的save()】时需要传入的参数 * */ //3.3 通过反射技术invoke(),执行目标对象obj的目标方法method【save()】 //save()被调用时传入的参数是100,"海绵宝宝" method.invoke(obj,100,"海绵宝宝"); }
@Test public void getField() throws NoSuchFieldException, IllegalAccessException, InstantiationException { Class> clazz = Process.class; Field field = clazz.getDeclaredField("name"); System.out.println(field.getType().getName()); System.out.println(field.getType()); Object obj = clazz.newInstance(); field.setAccessible(true); /*注意:!需要设置两个参数:那个对象的属性值,以及要设置 什么值, set(m,n) m-给那个对象设置值, n-给这个对象的属性设置一个 什么值 */ field.set(obj,"张修雨"); //注意还是需要指定获取的是那个对象的这个属性的值 System.out.println(obj); }
@Test public void getFunctiom() throws Exception { Class> clazz = Person.class; /** * 本方法的参数列表是getDeclaredMethod(name,x,y,z,...) * name:指的是要获取方法的名字 * z,x,e,...可变参数,指的是要获取方法的参数类型,注意是字节码对象".class" */ Method meth = clazz.getDeclaredMethod("add", String.class, int.class); Object obj = clazz.newInstance(); //如果想要执行私有的方法,也要设置私有可见 meth.setAccessible(true); /* invoke()用来调用目标方法,参数1是执行那个对象的这个方法 后续的参数是执行目标方法时传入的参数,这个参数是可变参数,根据 目标方法的具体情况来写*/ meth.invoke(obj,"张修雨",24); } }
如果一个类存在的意义就是为指定的另一个类,可以把这个类放入另一个类的内部。
就是把类定义在类的内部的情况就可以形成内部类的形式。
A类中又定义了B类,B类就是内部类,B类可以当做A类的一个成员看待:
1) 内部类可以直接访问外部类中的成员,包括私有成员
2) 外部类要访问内部类的成员,必须要建立内部类的对象
3) 在成员位置的内部类是成员内部类
4) 在局部位置的内部类是局部内部类
.创建内部类对象,使用内部类的资源
/*外部类名.内部类名 对象名 = 外部类对象.内部类对象*/
Outer.Inner oi = new Outer().new Inner();
oi.delete();
System.out.println(oi.sum);
//4.调用外部类的方法--这样是创建了一个外部类的匿名对象,只使用一次
new Outer().find();
}
}
//1.创建外部类 Outer
class Outer{
//1.1创建外部类的成员变量
String name;
private int age;
//1.2创建外部类的成员方法
public void find(){
System.out.println("Outer...find()");
//6.测试外部类如何使用内部类的资源
//System.out.println(sum);--不能直接使用内部类的属性
//delete();--不能直接调用内部类的方法
/*外部类如果想要使用内部类的资源,必须先创建内部类对象
* 通过内部类对象来调用内部类的资源*/
Inner in = new Inner();
System.out.println(in.sum);
in.delete();
}
//2.创建内部类Inner--类的特殊成员
/*根据内部类位置的不同,分为:成员内部类(类里方法外)、局部内部类(方法里)*/
class Inner{
//2.1定义内部类的成员变量
int sum = 10;
//2.2定义内部类的成员方法
public void delete(){
System.out.println("Inner...delete()");
//5.测试内部类是否可以使用外部类的资源
/*结论:内部类可以直接使用外部类的资源,私有成员也可以!*/
System.out.println(name);
System.out.println(age);
find();
public static void main(String[] args) {
/*如何使用内部类的资源呢?*/
//4.可以创建内部类Inner2的对象进行资源访问
//Outer2.Inner2 oi2 = new Outer2().new Inner2();
//oi2.eat();
/*如果Inner2被private修饰,无法直接在外部创建对象,怎么办?*/
//7.我们可以通过外部类对象,间接访问私有内部类的资源
new Outer2().getInner2Eat();
}
}
//1.创建外部类Outer2
class Outer2{
//6.提供外部类公共的方法,在方法内部创建私有内部类的对象并调用内部类的方法
public void getInner2Eat(){
Inner2 in = new Inner2();//外部类中创建内部类对象
in.eat();
}
//2.创建成员内部类Inner2
/*成员内部类的位置:类里方法外*/
//5.将成员内部类被private修饰--私有化,只能在本类中访问,外部无法访问
private class Inner2{
//3.创建成员内部类中的普通方法
public void eat(){
System.out.println("Inner2...eat()");
}
}
总结:
成员内部类被Private修饰以后,无法被外界直接创建创建对象使用
所以可以创建外部类对象,通过外部类对象间接访问内部类的资源
public static void main(String[] args) {
/*如何访问内部类的show()呢?*/
//4.创建内部类对象并访问show()
//方式一:按照之前的方式,创建内部类对象并调用show()
//Outer3.Inner3 oi3 = new Outer3().new Inner3();
//oi3.show();
//方式二:创建匿名内部类对象来访问show()
//new Outer3().new Inner3().show();
/*现象:用static修饰成员内部类以后
* new Outer3().new Inner3()报错*/
//6.用static修饰成员内部类以后上面的创建语句都报错了,注释掉
/*我们可以通过外部类类名直接创建静态内部类对象,无须再创建外部类对象了*/
Outer3.Inner3 oi = new Outer3.Inner3();
oi.show();
//7.匿名内部类对象调用show()
new Outer3.Inner3().show();
//9.访问静态内部类中的静态资源--链式加载
Outer3.Inner3.show2();
}
}
//1.创建外部类Outer3
class Outer3{
//2.创建成员内部类Inner3
//5.内部类被static修饰--并不常用!!!浪费内存!
static class Inner3{
//3.创建普通成员方法
public void show(){
System.out.println("Inner3...show()");
}
//8.创建静态成员内部类中的静态成员方法
static public void show2(){
System.out.println("Inner3...show2()");
}
}
}
public static void main(String[] args) { // Outer3.Inner3 iu = new Outer3().new Inner3(); // iu.show();//调用内部类方法 // new Outer3().new Inner3().show(); /** * 现象:内部类被static修饰以后 new Outer3()报错 */ //6 Outer3.Inner3 io = new Outer3.Inner3(); io.show(); new Outer3.Inner3(); /** * 访问静态内部类中的静态资源可以链式加载 */ Outer3.Inner3.show2(); } } //1外部类 class Outer3{ //内部类 static class Inner3{ //普通方法 public void show(){ System.out.println("Inner3...shiw()"); } static public void show2(){ System.out.println("Inner3...shiw2()"); } } }
public static void main(String[] args) { //3.创建匿名内部类 //以前我们使用接口,需要先创建接口的实现类 //接着接口实现类需要添加接口中未实现的方法并实现 //最后需要创建接口实现类对象并进行调用,这样比较复杂 //如果有的方法只是需要使用一次,就没必要这么复杂,使用匿名内部类即可 /*相当于创建了接口实现类+重写接口中的方法+创建对象+调用功能*/ new Inner1(){ @Override public void save() { System.out.println("Inner1...save()"); } @Override public void get() { System.out.println("Inner1...get()"); } }.get();//触发实现后的get(),注意只能使用一个,使用一次 //new Inner2();匿名对象,相当于匿名内部类实现了抽象类中的抽象方法 new Inner2() { @Override public void drink() { System.out.println("今夜我要不醉不归~"); } }.drink(); new Inner3().power(); Inner3 i3 = new Inner3(); i3.power(); i3.study(); i3.study(); } } //1.创建接口 interface Inner1{ //2.定义接口中的抽象方法 void save(); void get(); } //2.创建抽象类 abstract class Inner2{ public void play(){ System.out.println("Inner2...play()"); } abstract public void drink(); } //3.创建普通类 class Inner3{ public void study(){ System.out.println("什么都阻挡不了我们学习的脚步"); } public void power(){ System.out.println("我们会越来越强的,光头强"); }
总结:
静态资源访问时不需要创建对象,可以通过类名直接访问
访问静态类中的静态资源可以通过”. . . ”链式加载的方式访问