黑马程序员视频学习笔记
视频笔记
面对对象
零碎
javac先编译,生成class文件然后java运行类,一个类中运行需要有一个主函数
1 | pulic static void main(String[] args){} |
类名首字母必须大写,如果设定了CLASSPATH,则不用在类名前面加上pulic
Javadoc.exe可以提取java中的注释文档,在写程序的时候,注意在最前面加上注释,包括程序用途,算法说明,在没有思路的时候可以一步一步写出每一步需要实现的功能,代码只是算法功能的另一种表述。
若不知道错误出在哪里可以部分注释用来缩小出错范围
8进制是三位的二进制,用0开头,16进制是四位的二进制,用0x开头。
一个2进制为一个bit,一个byte字节为8个二进制位
整数默认int 小数默认double
java里面+号可以表示运算符也可以表示成连接符,如Systen.out.println(a+’,’+b);
运算符
^异或,两边结果不同才为真,两边结果相同则为假
&&双与,只要左边为假,右边不运算,若想让右边参与运算,用&
||双或,只要左边为真,右边不运算,若想让右边参与运算,用|
进行循环条件判断的时候,用&&和||效率会更高
而对于位运算符,6&3代表转换为二进制110&011,则全为1则与后为1,否则为0,结果为010,所以6 & 3 = 2;6|3 ,只要二进制位有一个为1则结果为1,所以6|3 = 111(2进制)=7(10进制)
一个数异或同一个数两次,结果还是这个数(数据加密)
6,相当于对6的每一位进行取反,6全部取反加一得到-6,所以6 = -7
3<<2,表示3左移两位,即0011变为1100,3<<2=12,左移几位就是该数乘以2的几次方,即为<<可以完成2的次幂运算;而对于>>,右移几位就是除以2的几次幂,对于高位保持原来的数字,以保证符号不变。而对于>>>,无论原来高位是什么,都用0来补
想要运算变得高效,首选位运算
三元运算符
(条件表达式)?表达式1:表达式2;
a) 判断条件表达式,结果为一个布尔值。
b) true**,运算结果为表达式**1
c) false**,运算结果为表达式**2
局部代码块可以定义局部变量的生命周期
switch关键字:switch,case,break,default,可以处理的数据类型是byte,short,int,char
break 跳出当前循环,并结束此次循环
do while相比while,无论条件是否满足,至少执行一次循环内容,应用相对比较少。
for(初始化表达式;循环条件表达式;循环后的操作表达式)
其中初始化表达式只在最开始进行执行(执行一次),然后运行条件表达式判断是否循环,如果是则进入循环内执行语句,然后运行循环后的操作表达式。
制表符
\t为制表符,用于将数据对齐 \n回车 \b退格 \r按下回车键
windows系统中,回车符由两个符号组成,为\r\n linux中回车符为\n
转义字符对后面的字符含义进行转义,如果想输出”,则需要使用\”,在前面加上\
函数
定义函数格式
修饰符 返回值类型 函数名(参数类型 形式参数1,参数类型 形式参数2,…)
{
执行语句;
return 返回值;
}
如果函数名字有两个以上的单词,第一个单词首字母小写,从第二个单词开始要大写,需要去区分是类还是函数。
如果函数中没有返回值类型,则用void,可以省略return。
函数中可以调用其他函数,但是不可以定义其他函数
在内存中,java内存为栈的形式,先进后出,调用的函数结束后即被清理出内存
函数的重载
同一个类
同名
参数个数不同or参数类型不同
利用好函数的重载,同样功能的函数起一样的名字,利用传递参数的不同加以区分,同时利用之前已有的函数功能,进行调用,提高代码的复用性。
查表法
查表法:在数据中出现对应关系,而且对应关系的一方是有序的数字编号,并作为角标使用,这时候就要想到数组的使用。可以将这些数据存储到数组中,根据运算的结果作为角标直接去查数组中对应的元素即可
数组
数组的格式
元素类型[] 数组名 = new 元素类型[元素个数或数组长度]
二维数组
在栈中开辟内存给arr,在堆中开辟内存,产生一个实体存放每个小数组的地址,初始值为null,然后新建三个小数组实体,每个小数组的初始值为0,将每个小数组的地址赋给大数组,将null改变为三个地址,然后将arr指向大数组的地址,这样二维数组初始化完毕
二维数组赋值:
1 | int[][] arr = new int[3][2]; |
成员变量与局部变量
成员变量定义在类中,整个类中都可以访问;局部变量定义在函数,语句,局部代码块中,只在所属的区域有效。
成员变量存在于堆内存的对象中;局部变量存在于栈内存的方法中。
成员变量随着对象的创建而存在,随着对象的消失而消失;局部变量随着所属区域的执行而存在,随着所属区域的结束而释放。
成员变量都有默认初始化值;局部变量没有默认初始化值。
封装
隐藏对象的属性和实现细节,仅对外提供公共访问方式,笔记本就是种封装
将类中变量定义为private,为了改变变量的值,必须要经过类所规定的方法(对设置的变量给予约束)
封装原则:将不需要对外提供的内容全都隐藏起来;把属性都隐藏,提供公共方法(set,get)对其访问,使得对数据可控,set一般为viod,而get返回类型和属性相同
private为权限修饰符,修饰成员,不能修饰局部。私有的内容只在本类中有效。
私有只是封装的一种体现,java中最小的封装体是函数
构造函数
名称与类相同,不需要返回值,用于给对象初始化,构造创造对象时调用的函数,首字母大写,构造函数里面可以return用以结束函数,但是很少见
创建对象都必须要通过构造函数进行初始化;如果一个类中没有定义过构造函数,那么该类中会有一个默认的空参数构造函数。如果在类中定义了指定的构造函数,那么类中的默认构造函数就没有了。
当方法中的成员变量和局部变量重名,那么成员变量会变成局部变量,当方法结束调用后出栈,没有改变成员变量的值,可以用this来区分,如this.name = name。this就是所在函数所属对象的引用。简单说,哪个对象调用了this所在的函数,this就代表哪个对象。
关键字
this关键字
如果要在构造函数中调用构造函数,不能直接用Person();,因为这句话相当于this.Person,对象还没有初始化不可被调用,这时候可用this(待传入参数),相当于直接给这个对象进行初始化。
this可在构造函数中调用其他构造函数,但是只能定义在构造函数的第一行,因为初始化动作要先执行
只要在本类中调用了本类的对象,一般都要用this
static关键字
修饰数据,可实现数据共享,在对象之前出现,可以被类名调用
特点:
static是一个修饰符,用于修饰成员(成员变量和成员函数)。
static修饰的成员被所有的对象共享。
static优先于对象存在,因为static的成员随着类的加载就已经存在了。
static修饰的成员多了一种调用方式,即可以直接被类名所调用,调用格式为类名.静态成员。
静态修饰的数据是共享数据,对象中存储的是特有数据。
成员变量(实例变量)与静态变量(类变量)的区别:
- 两个变量的生命周期不一样(静态变量生命周期太长,用以减少占用内存空间)
随着对象的创建而存在,随着对象的回收而释放;静态变量随着类的加载而存在,随着类的消失而消失
- 调用方式不同
成员变量只能被对象调用,静态变量可以被对象和类调用,不建议用对象调用
别名不同
成员变量也成为实例变量,静态变量成为类变量
数据存储位置不同
成员变量数据存储在堆内存(堆中存储的都是实体)的对象中,所以也叫对象的特有数据。静态变量数据存储在方法区(的静态区),也叫对象的共享数据
静态变量前省略了类名,非静态变量前省略了this
静态使用注意事项
静态方法只能访问静态成员(因为其先于对象存在,无法访问对象中存在的成员变量和函数),静态方法可直接被类名调用。非静态既可以访问静态,又可以访问非静态。
静态方法中不可使用this或者super关键字
主函数是静态的(不要在主函数中定义其他函数,要将其他函数封装在类中,在主函数中创建对象,调用对象的函数即可)
主函数特殊之处
public static void main(String[],args)
格式是固定的
被jvm所识别和调用
public:因为权限是最大的
static: 不需要对象的,直接用主函数所属类名调用即可
void:主函数没有具体的返回值
main: 函数名,不是关键字,只是一个jvm识别的固定的名字
String[] args:这是主函数的参数列表,是一个数组类型的参数,而且元素都是字符串类型
主函数所留了一个args,相当于new String args[0],留给使用者指定参数,直接java 类名 加所要传入参数
类被加载进方法区,方法区用来存放类和静态方法的代码,也加方法表,运行时候进栈存储局部变量
protected关键字
指明“就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它却是可以访问的”。(protected提供了包内访问权限。)
final关键字
final 关键字:
1、 final是一个修饰符,可以修饰类,方法,变量
2、 final修饰的类不可以被继承
3、 final修饰的方法不可以被覆盖
4、 final修饰的变量是一个常量,只能赋值一次变量命名与函数一样,常量所有字母都大写,单词之间用_连接)成员一旦被final ,一般会加静态 static final int x = 7;
public static final 全局变量
用final修饰变量原因
其实在程序中如果一个数据是固定的,那么直接使用这个数据就可以了,但是这样阅读性差,所以给该数据起个名称,而且这个变量名称的值不能变化,所以加上final固定。固定常量一律用final修饰
写法规范
常量所有字母都大写,多个单词,中间用_连接。
可能使用到final的三种情况:数据、方法和类
final数据
不能改变的数据需要是基本数据类型,以final关键字表示,在对常量定义的时候,必须对其赋值。
一个既是static又是final的域只占据一段不能改变的存储空间。
对对象的引用用final修饰,使其不能指向另一个对象,但是对象本身可以被修改。
不能因为某数据是final类型就认为可以在编译时知道它的值,final指向只是不能再次指向另一个新的对象,不代表值不能被改变。
如果数据是static的,在装载时已被初始化,而不是每次创建新对象时都初始化。
允许生成空白final,即被声明为final但又未给定初值的域。必须在域的定义处或者每个构造器中用表达式对final进行赋值。(如果不在构造器处初始化空白final,会编译报错。)
final参数
允许在参数列表中以生命的方式将参数指明为final,意味着无法在方法中更改参数引用所指向的对象。
可以读参数,却无法修改参数,主要用来向匿名内部类传递数据。
final方法
使用final方法原因:
一:把方法锁定,以防任何继承类修改它的定义。处于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。
二:效率。以前会为了效率,将一个方法指明为final,同意编译器将针对该方法的所有调用都转为内嵌调用。应该让编译器和jvm去处理效率的问题,只有想要明确禁止覆盖时,才将方法设置成final。
final和private关键字
类中所有的private方法都隐式地指定为是final的。“覆盖”只有在某方法是基类的接口的一部分时才会出现,即必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果方法为private,它就不是基类接口的一部分。它仅是一些隐藏于类中的程序代码,只不过是具有相同名称而已。
final类
当某各类的整体定义为final时(将关键字final置于它的定义之前),就表明了不打算继承该类,也不允许其他人这么做。
在final类中可以给方法添加final修饰词,但不会增添任何意义。
内存
内存划分
寄存器 CPU处理
本地方法区 和操作系统相关
方法区
栈内存 存储局部变量,而且变量所属的作用域一旦结束,变量自动释,方法(函数)进栈
堆内存 存储数组(数组就是对象)和对象,凡是new建立在堆中
堆的特点:
- 每一个实体都有首地址值
- 堆内存中的每一个变量都有默认初始化值,根据类型的不同而不同。整数0,小数0.0或0.0f,boolean false,char ‘\u0000’相当于空格的空位
- 垃圾回收机制,由程序控制不定时清理
内存过程
StaticDemo2这个类被加载进方法区,因为其为非静态方法,有一个this用于表示当前对象,同时还有一个默认的空构造函数,因为这个类中有非静态方法,于是static main进入静态方法区,存储相应代码。运行main函数,main进栈。加载Person.method(),jvm寻找Person.class文件,找到后将Person类加载进方法区中非静态方法中,包括Person的代码,也包括一个this,存放非静态方法Person(),show(),将静态方法method存放金静态方法区,其中也包括country这个静态变量。运行method,method进栈,输出静态变量,没有在堆中开辟空间,运行结束后弹栈。
建立新对象p,在堆中开辟内存空间地址为0x0056,初始化name=null,age=0。然后执行Person的构造函数,构造函数进栈,this赋值为此对象地址,将name和age赋值,接下来执行this.name=name和this.age=age,将堆中对象的name和age赋值改变。初始化结束后构造函数弹栈,将p指向0x0056。执行show(),在方法区中寻找show,show进栈,有一个this所属,输出类所属的静态变量country,this所属(对象)的name和age,进行输出,结束后show弹栈。Main执行return后,main也弹栈。
对象创建过程
变量在任何方法(包括构造器)被调用前初始化。
初始化的顺序是先静态对象(如果他们未因前面的对象创建过程而被初始化),然后是“非静态”对象,然后是构造器。
静态对象会随着类的加载而初始化,之后不会再次初始化。
实例初始化在构造器之前。
假设有个Dog类
即使没有显式地使用static关键字,构造器实际上也是静态方法。因此,当首次创建爱你类为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
然后载入Dog.class(创建一个Class对象),有关静态初始化的所有动作都会执行。因此静态初始化只在Class对象首次加载的时候执行一次。
当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。
这块存储空间就会被清零,这就自动地将Dog对象中的所有基本类型数据都设置成了默认值,引用被设置成null。
执行所有出现于字段定义处的初始化动作。
执行构造器。
静态
静态什么时候用?
1、 静态变量
当分析对象中所具备的成员变量的值都是相同的,这时这个成员就可以被静态修饰。只要数据在对象中都是不同的,那就是对象的特有数据,必须在对象中是非静态的。如果是相同的数据,对象不需要做修改,只需要使用即可,不需要存储在对象中,定义成静态的。
2、 静态函数
函数是否用静态修饰,就参考一点,就是该函数功能是否有访问到对象的特有数据。简单点即从代码中看,该功能是否需要访问非静态的成员变量,如果需要,该功能就是非静态的;如果不需要,就可以将该功能定义成静态的。当然也可以定义为非静态,但是非静态需要被对象调用,而仅创建对象调用非静态的没有访问对象特有数据的方法,该对象的创建没有意义。
3、静态代码块
1 | static |
随着类的加载而执行
作用:用于给类进行初始化。
一般用于全静态变量的类
构造代码块
1 | { |
构造代码块为在类中的代码块,不加static修饰符,用以给所有对象初始化。与构造函数区别是构造函数是给对应的对象有针对性的初始化。构造代码块为对象的通用部分,构造函数为对象的特性部分。
若在函数中为局部代码块,用以限制变量的周期。
单例设计模式
强制不让创建对象
private ArrayToll(){}该类中的方法都是静态的,所以该类是不需要创建对象的。为了保证不让其他成员创建该类对象,可以将构造函数私有化
设计模式:对问题行之有效的解决方式。其实是一种思想
单例设计模式解决问题:可以保证一个类在内存中的对象唯一性
必须对多个程序使用同一个配置对象时,就需要保证该对象的唯一性
如何保证对象唯一性?
(1) 不允许其他程序用new创建该类对象
(2) 在该类中创建一个本类实例
(3) 对外提供一个方法让其他程序可以获取该对象
步骤
(1) 私有化该类的构造函数
(2) 通过new在本类中创建一个本类的对象
(3) 定义一个公有的方法,将创建的对象返回
饿汉式
开发更多
1 | //类一加载,对象就已经存在了 |
调用 Single ss = Single.getInstance();
当调用getInstance()的时候,此函数进栈,将s的地址赋值给ss,然后弹栈,这样ss就指向了s所指向的对象
懒汉式
面试更多 延迟加载形式(懒汉式)问题:如果被多线程调用,可能不能保证唯一性
1 | //类加载进来,没有对象,只有调用了getInstance方法才创建对象,延迟加载形式 |
继承
class Student extends Person
Student 子类 Person父类(超类,基类)
继承弊端:打破封装性
继承的好处
提高了代码的复用性
让类和类之间产生了关系,给第三个特征多态提供了前提
java中支持单继承,不直接支持多继承,但对C++中的多继承机制进行了改良
单继承:一个子类只能有一个直接父类
多继承:一个子类可以有多个直接父类(java中不允许,进行改良)
不直接支持,因为多个父类中有相同成员,会产生调用不确定性,在java中通过“多实现”的方式来实现
java支持多层(多重)继承
C继承B,B继承A。就会出现继承体系。
使用一个继承体系
查看该体系中的顶层类,了解该体系的基本功能
创建体系中的最子类对象,完成功能的使用
在子父类中,成员的特点体现
- 成员变量
子类中与父类同名变量,值与子类相同。当本类中的成员和局部变量同名用this区分。
当子父类中的成员变量同名可用super区分父类。
this 和 super的用法很相似
this:代表一个本类对象的引用
super:代表一个父类空间
- 成员函数
当子父类中出现成员函数一模一样的情况,会运行子类的函数,这种现象称为覆盖操作。
这是函数在子父类中的特性。
函数两个特性:
(1) 重载 同一个类中
(2) 覆盖 子类中,覆盖也称为重写,覆写。override,函数声明需要一致
覆盖注意事项
子类方法覆盖父类方法时,子类权限必须要大于等于父类的权限
静态只能覆盖静态,或被静态覆盖
什么时候使用覆盖操作
当对一个类进行子类的扩展时,子类需要保留父类的功能声明,但是要定义子类中该功能的特有内容时,就使用覆盖功能完成
- 构造函数
在子类构造对象时,发现访问子类构造函数时,父类也运行。原因:在子类的 构造函数中第一行有一个默认的隐式语句。super();
子类的实例化过程:子类中所有的构造函数默认都会访问父类中的空参数的构造函数
子类实例化访问父类构造函数原因:
子类继承了父类,获取到了父类中的内容(属性),所以在使用父类内容之前要先看父类是如何对自己的内容进行初始化的,所以子类在构造对象时必须访问父类的构造函数。为了完成这个必须的动作,就在子类的构造函数函数中加入了super();语句.如果父类中没有定义空参数构造函数.那么子类的构造函数必须用super明确要调用父类中哪个构造函数。同时子类构造函数中如果使用this()调用了本类构造函数时,super就没有了,因为this和super都只能定义在第一行,所以只能有一个。但是可以保证的是,子类中肯定会有其他的构造函数访问父类的构造函数。
注意:super语句必须要定义在子类构造函数的第一行,因为父类的初始化动作要先完成。
所有的类都有 extends Object
继承内存图解,子类对象中继承了父类对象的num,用super关键字进行引用
子类中不能直接访问父类中私有的内容
运行Zi的构造函数,super()为Fu(),此时的show()被子类覆盖,运行zi show…0。通过父类super初始化父类内容时,子类的成员变量并未显示初始化,等父类super()父类初始化完毕后,才进行子类的成员变量显示初始化。即super后num=8
一个对象实例化过程
Person p = new Person();
JVM会读取指定路径下的Person.class文件,并加载进内存,并会先加载Person的父类(如果有直接的父类的情况下)
在堆内存中开辟空间,分配地址
并在对象空间中,对对象的属性进行默认初始化
调用对应的构造函数,进行初始化
在构造函数中,第一行会调用父类中的构造函数进行初始化
父类初始化完毕后,再对子类的属性进行显示初始化
再进行子类构造函数的特定初始化
初始化完毕后,将地址值赋值给引用变量
组合与继承
组合:在一个类中持有另一个类的引用。
“is-a”(是一个)的关系是用继承表达的,而“has-a”(有一个)的关系则是用组合来表达的。
判断要用组合还是继承,看是否需要从新类向基类进行向上转型,如果必须向上转型,则继承是必要的。
代理
如果使用继承,会将父类所有方法暴露给子类,可以使用代理,可以选择只提供在成员对象中的方法的某个子集。
抽象类
abstract class Demo
{ abstract void show();
}
1、 方法只有声明没有实现时,该方法就是抽象方法,需要被abstract修饰。抽象方法必须定义在抽象类中,该类也必须要被abstract修饰
2、 抽象类不可以被实例化(不可以被new),调用抽象方法没意义。
3、 抽象类必须有其子类覆盖了所有的抽象方法后,该子类才可以实例化。否则,该子类还是抽象类
接口
当一个抽象类中的方法都是抽象的时候,这时可以将该抽象类用另一种形式定义和表示,就是接口 interface。
定义接口用的关键字不是class,是interface
接口中常见的成员:而且这些成员都有固定的修饰符
全局常量 public static final
抽象方法 public abstract
由此有接口中的成员都是公共的权限
1 | interface Demo{ |
类与类之间是继承关系,类与接口之间是实现关系,接口与接口之前是继承关系,而且可以多继承
接口不可以实例化,只能由实现了接口的子类并覆盖了接口中所有的抽象方法后,该子类才可以实例化。否则,这个子类就是一个抽象类
1 | class DemoImpl implements /*实现*/Demo{ |
在java中不直接支持多继承,因为会出现调用的不确定性。所以java将多继承机制进行了改良,在java中变成了多实现。
一个类可以有多个接口。
class Test implements A,Z 多实现,如果A,Z中方法名一样,也不会报错,因为A,Z中无方法体,若Test中覆盖此同名方法,则A,Z中方法会被同时覆盖,不会产生不确定性,而继承中因为存在方法体,所以不能多继承。
一个类在继承另一个类的时候,还可以实现多个接口。先继承,再实现接口。
class Test extends Q implements A,Z
接口的出现避免了单继承的局限性。接口与接口之前是继承关系,而且可以多继承
interface QQ extends CC,MM
接口的特点
接口是对外暴露的规则
接口是程序的功能扩展
接口的出现降低耦合性
接口可以用来多实现
类与接口之间是实现关系,而且类可以继承一个类的同时实现多个接口
接口与接口之间可以有继承关系
抽象类和接口的异同点
相同点:都是不断向上抽取而来的
不同点:
抽象类需要被继承,而且只能单继承。
接口需要被实现,而且可以多实现
抽象类中可以定义抽象方法和非抽象方法,子类继承后可以直接使用非抽象方法。
接口中只能定义抽象方法,必须由子类去实现
抽象类的继承,是is a 关系。在定义该体系的基本共性内容,基本功能
接口的实现是like a 关系。在定义体系额外功能
在不同的领域中,有不同的分析方式
接口类型对象指向自己的子类对象。接口类型的引用,用于接收(指向)接口的子类对象
1 | interface USB{ |
主函数中
1 | useUSB(null); |
枚举
在某些情况下,一个类的属性是有限而且固定的,如下面的棋子类,只有两个对象,白棋和黑棋。这种实例有限而且固定的类,在java中称为枚举类,枚举类的关键字是enum,此类中有两个枚举属性BLACK和WHITE,代表黑子与白子。
1 | //棋子类,枚举类,为构造器私有,不能直接创建 |
此枚举的构造器权限使用private表明子类不可以通过外部创建,只能通过此类的内部创建,为了保证此对象只有黑子与白子两种类型。红色代码列出了枚举值,实际上是调用私有构造器创建此对象,等效于:
1 | public static final Chessman BLACK = new Chessman("●"); |
因为BLACK与WHITE两个属性是静态的,要获取黑子或白字,可使用以下代码
1 | Chessman.BLACK.getChessman(); |
文档注释
对自己所写类添加说明文档,用javadoc
类的文档描述应该包括:
这个类的作用,包含的内容
@author Hugh
@version V1.0
注释格式为
/**
*/
对于每一个方法,描述其作用,@param arr 接收对象描述 @return 返回值描述
为了能使用javadoc,要将class前面加上public,这样类名和文件名必须要一致
使用语句为 javadoc -d myhelp(自己要放的文件名) –author –version ArrayTool.java(要查看的java文件)然后查看index网页,只提供公有权限的
使用时候若class文件与测试用java文件不在同一目录,使用set classpath=.;c:\myclass(相应目录).;表示当前目录(用以编译当前测试文件),然后再设置对方所给class文件存放目录。若已经设置好对方classpath目录,则使用set classpath=.;%classpath%
若要清楚classpath,使用 set classpath=,查看路径为set classpath
多态
多态:某一类事物的多种存在形态
用父类类型指向子类对象,猫这类事物既具备了猫的形态,又具备了动物的形态,这就是对象的多态性。一个对象对应着不同类型
在代码中体现:父类或接口的引用指向了其子类对象
1 | Animal c = new pig(); |
多态的好处:
提高了代码的扩展性,前期定义的代码可以使用后期的内容
多态的弊端:
前期定义的内容不能使用(调用)后期子类的特有内容
多态的前提:
必须有关系,继承,实现。
要有覆盖
Animal a = new cat();//自动类型提升,猫对象提升为了动物类型,但特有功能无法访问,
作用为限制对特有功能的访问
专业讲:向上转型,将子类型隐藏,就不能使用子类型的特有方法
若父类中的内容被子类覆盖,调用向上转型对象的方法所实现的为子类的内容
若想使用特有内容,则将对象进行向下转型
1 | Cat c = (Cat)a;//向下转型的目的是使用子类中的特有方法 |
注意:对于转型,自始至终都是子类对象在做着类型的变化
对对象类型的判断
instanceof:用于判断对象的具体类型,只能用于引用数据类型判断,通常在向下转型前用于健壮性的判断。
if(a instanceof Cat){}
转型之前加入逻辑判断,加强代码的健壮性
多态时成员的特点
- 成员变量
编译时:参考引用型变量所属的类中是否有调用的成员变量,有,编译通过;没有,编译失败。
运行时:参考引用型变量所属的类中是否有调用的成员变量,并运行所属类中的成员变量
简单说:编译和运行都参考等号的左边
- 成员函数(非静态,重点)
编译时:参考引用型变量所属的类中是否有调用的函数,有,编译通过;没有,编译失败。
运行时:参考的是对象所属的类中是否有调用的函数
简单说:编译看左边,运行看右边
- 静态函数
编译时:参考引用型变量所属的类中是否有调用的静态方法
运行时:参考引用型变量所属的类中是否有调用的静态方法
简单说:编译和运行都参考等号的左边
其实静态方法是不需要对象的,直接类名调用即可
内部类
内部类:将一个类定义在另一个类里面,对里面那个类成为内部类(内置类,嵌套类)
1 | class Outer{ |
在主函数中编译,产生Outer.class和Outer$Inner.class文件
若要访问一个类中私有变量,最简单方法是在该类中定义内部类,则可以直接访问该私有变量
内部类访问特点
内部类可以直接访问外部类中的成员
外部类要访问内部类,必须建立内部类的对象
一般用于类的设计。分析事物时发现该事物描述中还有事物,而且该事物还在访问该事物的内容,这时把这个事物定义为内部类来描述
直接访问外部类中的内部类成员
1 | Outer.Inner in = new Outer().new Inner();in.show();//必须先建立外部类对象 |
如果内部类是静态的,相当于一个外部类
1 | Outer.Inner in = new Outer.Inner();in.show();//不需建立内部类对象,此静态方法中只能使用静态变量 |
如果内部类是静态的,成员是静态的。
1 | Outer.Inner.function(); |
当内部类中定义了静态成员,该内部类必须被静态修饰,或者该成员被final修饰
外部类只能用public和default默认修饰符,内部类四种修饰符都可以用
内部类可以直接访问外部类中成员的原因:内部类持有外部类的引用,外部类名.this
外部类和内部类拥有同名的变量和方法时,内部类有需要进行访问外部的同名变量或方法,可以通过外部类.this.方法名/变量名进行访问
- 成员内部类
当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
- 外部类.this.成员变量
- 外部类.this.成员方法
成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。
- 局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。
- 匿名内部类
匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。
- 静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。只有静态内部类,才能在类中申明静态方法,普通内部类申明静态方法会报错。
内部类问题解释
为什么成员内部类可以无条件访问外部类的成员?
编译器会默认为成员内部类添加了一个指向外部类对象的引用(this)。从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。
为什么局部内部类和匿名内部类只能访问局部final变量?
如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。为了避免在内部类的方法中改变参数造成数据不一致性,局部内部类和匿名内部类中访问成员变量需要被final修饰。
一个类文件中,只能有一个公共的(**public**)外部类,但可以有多个(public、default、private)内部类,多个(default)外部类,private不能用来修饰外部类!
匿名内部类
匿名内部类。就是内部类的简写格式
必须有前提:内部类必须继承或者实现一个外部类或者接口
匿名内部类:其实就是一个匿名子类对象(因此如果父类中无此方法会报错)
格式: new 父类or接口 () { 子类内容 }
new demo()即为匿名内部类,因为是对象所以可以直接调用方法
注意:在匿名内部类中,要将方法权限设置为public,否则无法被实现
通常使用场景之一:当函数参数是接口类型时,而且接口中的方法不超过三个。可以利用匿名内部类作为实际参数进行传递
new Object(){}相当于创立了子类对象,而Object obj=子类对象,相当于向上转型(多态),隐藏了子类特有属性,编译看左边,Object类中无show方法,因此会编译失败
对象构造:显示初始化在构造器初始化之后
new Zi()对象后,先执行Zi构造函数,默认第一行为super(),此时调用super中的show,因为子类中show被覆盖,因此输出show…,而此时num还没有被显示初始化,因此num为0.然后执行显示的初始化,这时候num为9
创立对象时,先默认初始化,即num=0,然后Zi()进栈,执行父类构造函数,show函数在子类对象中被覆盖,执行子类show,父类构造函数出栈后进行显示初始化,此时num=9,然后进行构造代码块输出化,然后才是自定义构造函数语句输出
首先为Fu和Zi开辟内存空间,然后执行子类的构造函数,先执行super(),Fu中super为Object,然后进行显示初始化,此时Fu中num=9,再执行父类中构造代码块初始化,因此第1个输出Fu。然后执行show()方法,因为父类中show被覆盖,所以执行子类的,而子类中还未进行显示初始化,因此子类中num=0,第2步子类show()输出zi show num=0。然后子类中父类构造函数执行完毕出栈,执行子类显示初始化,num=8,然后执行构造代码块初始化,因此第三步输出Zi.然后进行Zi类中show方法,第4步输出zi show num=8。
异常
异常:运行时期发生的不正常情况
java中用类的形式对不正常情况进行了描述和封装对象。描述不正常的类,成为异常类。以前正常流程代码和问题处理代码相结合,现在将正常流程代码和和问题处理代码分离,提高阅读性。
不同的问题用不同的类进行具体的描述,比如角标越界,空指针等。
问题很多,意味着描述的类也很多,将其共性向上抽取,形成了异常体系。最终问题(不正常情况)就分成了两大类。
Throwable:无论是error,还是异常,问题,问题发生就应该可以抛出,让调用者知道并处理。
//该体系的特点就在于Throwable及其所有的子类都具有可抛性。
可抛性:通过两个关键字来体现的。throws,throw,凡是可以被这两个关键字所操作的类和对象都具有可抛性。
|–1、一般不可处理的。Error
特点:是由jvm抛出的严重性的问题。这种问题发生一般不针对性处理。直接修改程序
|–2、可以处理的。Exception
发生错误时,对错误地方进行对象封装, throw一个新错误对象给调用者,虚拟机,然后在控制台输出,实际开发时会以日志的形式存储起来。错误地方后面的功能不予执行,因此抛出错误对象可以结束函数。
throw 抛出对象 throw new …某Exception()
对于角标是正数不存在,可以用角标越界表示;对于负数为角标的情况,准备用负数角标异常来表示。负数角标这种异常在java中并没有定义过,那就按照java异常的创建思想,面向对象,将负数角标进行自定义描述,并封装成对象。这种自定义的问题描述称为自定义异常。
注意:如果让一个类成为异常类,必须要继承异常体系,因为只有成为异常体系的子类才有资格具备可抛性。才可以被两个关键字所操作。throws throw。
extends RuntimeException
如果在函数内发生了异常,应该在函数中进行声明 throws 引用在函数声明上,throw用在函数内;调用发生异常函数,必须要处理,处理方式之一为抛出。因为继承了父类的异常体系,在重载自定义的异常构造函数时,若要进行String提示输出,可以用super(Str)来进行相关语言提示。
异常的分类
编译时被检测异常:只要是Exception和其子类都是,除了特殊子类RuntimeException体系。这种问题一旦出现,希望在编译时就进行检测,让这种问题有对应的处理方式。
编译时不检测异常(运行时异常)就是Exception中的RuntimeException和其子类
这种问题的发生无法让功能继续,运算无法进行,更多是因为调用者的原因导致的或者引发了内部状态的改变导致的。这种问题一般不处理,直接编译通过,在运行时让调用者调用时的程序强制停止,让调用者对代码进行修正。
如果不需要将问题暴露,编译通过在运行时停止;若需要将问题暴露,则在编译时报错 因此自定义异常时,继承Exception或者RuntimeException
throws和throw区别
throws使用在函数上,throw使用在函数内
throws抛出的是异常类,可以抛出多个,用逗号隔开。throw抛出的是异常对象,一次只能抛出一个
异常处理的捕捉形式
这是可以对异常进行针对性处理的方式。
具体形式是:
try{需要被检测异常的代码}
catch(异常类 变量){处理异常的代码}//该变量用于接收发生的异常对象
finally{一定会被执行的代码}
这个代码块是一个整体
在try中放入待检测代码块(其中为局部变量),此对象调用方法抛出一个异常对象,此对象被catch捕捉,e = new异常对象,然后继续输出负数角标异常,因为问题已经被解决,因此程序可以向下运行输出over
如果输出e.getMessage(),则可以输出此异常详细信息。getMessage()为父类throwable中的方法。一般直接打印对象,输出为对象地址和哈希值,但是异常对象相当于使用e.toString(),输出为异常类名加信息。
e.printStachTrace(),将其追踪输出至标准错误流(jvm默认的异常处理机制)
该体系的特点:子类的后缀名都是用其父类名作为后缀,阅读性很强
如果有多个catch的情况,在抛出的时候throws 多个异常,然后写多个catch即可。
如果在多catch情况下,出现了父类Exception e的处理情况,一定要放在多catch的最后,不然放在最前面其他catch永远不执行(多catch下父类的catch放在最后,否则编译失败)
只要使用到了声明异常的方法,就要try
异常处理的原则
函数内容如果抛出需要检测的异常,那么函数上必须要声明。否则必须在函数内用try,catch捕捉,否则编译失败。
如果调用到了声明异常到的函数,要么try,catch,要么throws,否则编译失败
什么时候catch,什么时候throws
功能内部可以解决,用catch。解决不了用throws告诉调用者,由调用者解决。
一个功能如果抛出了多个异常,那么调用时必须有对应多个catch进行针对性的处理。
内部有几个需要检测的异常就抛几个异常。抛出几个,就catch几个
finally是一定会被执行到的,除非在catch中用到了System.exit(1),直接退出jvm。
finally通常用于关闭(释放)资源,如数据库查询出异常后要关闭连接,关闭连接就在finally中
没catch就没处理
try catch finally代码块组合特点:
try catch finally
try catch(多个) 当没有必要资源需要释放时,可以不用定义finally
try finally 异常无法直接catch处理,但是资源需要关闭
1 | void show throws Exception(){ |
异常转换:捕捉一个异常,暴露出来的是另一个异常,因为此捕捉异常无法被处理,因此转换为可以被处理的异常
catch(MaoYanException e){
System.out.println(e.toString());
test();
//可以对电脑进行维修
//throw e;
throw new NoPlanException(“课时进度无法完成”+e.getMessage());
}
如果发生问题,内部解决,没有给外部抛出,就是隐藏了问题。异常的封装:解决那些内部解决的,抛出需要被外界知道的异常类信息。
异常的注意事项
子类在覆盖父类方法时,如果父类的方法抛出了异常,那么子类的方法只能抛出父类的异常或该异常的子类(可以不抛)
如果父类抛出多个异常,那么子类只能抛出父类异常的子集
简单说,子类覆盖父类,子类只能抛出父类的异常或者子类或者子集
注意:如果父类的方法没有抛出异常,子类覆盖时绝对不能抛,就只能try
子类重写的方法可以抛出任何运行时异常(RuntimeException和ArithmeticException属于运行时异常)
Object
Object:所有类的根类
Object是不断抽取而来,具备着所有对象都具备的共性内容
常用的共性功能:
boolean equals(Object obj):判断两个对象是否相等,仅当引用同一个对象才返回true,比较的也是地址
一般都会覆盖此方法,根据对象的特有内容,建立判断对象是否相同的依据
一般向下转型要进行健壮性判断 用instanceof进行判断,扔出异常(运行时异常)告诉用户
if(classA instanceof classB)
hashCode(),获得哈希地址值,直接输出对象引用=Integer.toHexString(对象.hashCode());
可以根据对象的特性不同,输出不同的哈希值
比较对象equals是否相同:1、判断哈希值是否相同。2、判断内容是否相同
重写equals时候,一般要重写hashCode(),使得相同的对象要有相同的哈希地址值
getClass() 返回此Object的运行时类,即当前对象所属的字节码对象,类型为Class
1 | class Class{//字节码文件 |
Person p = new Person(20);
先在堆里面产生一个Person.class字节码文件对象,再根据字节码文件对象产生Person对象
Class clazz1 = p.getClass();
可以使用clazz1.getname()即getConstructor()等方法。
toString()方法
输出为p.getclass().getName+”@”+Inthger.toHexString(p.hashCode())
如果想建立每个对象特有的字符串输出方法,就可以将toString()方法覆写
包(package)
对类文件进行分类管理;对类提供多层命名(名称)空间;写在程序的第一行;类名的全程是 包名.类名;包也是一种封装形式
在java文件第一行写package zc.Demo;
编译时候 javac -d . Hello.java,自动生成相应目录,然后运行java zc.Demo.Hello
如果两个java文件在不同的包下,一个需要用到另一个类,则如果直接用类名来新建对象会出错,因为类已经有包的所属,所以必须要明确其包名。记住:Demo这个名称是错的,正确名称是包名.类名 packa.Demo。如果两个java文件不在一个目录下,则编译有主函数的类时会找不到调用类,此时需要将被调用类包所在的路径设置为路径
set classpath=.;+路径。在包中,如果没有写public,就是被封装了。方法也不能使用默认权限,要使用public。
总结:包与包之间的类访问,被访问的包中的类必须是public的,被访问的包中的类的方法也必须是public的。
protected权限,第四种,只有在不同包中的子类才能使用,即必须要继承才能使用,直接在不同包中创建对象无法使用被保护的方法,其实也叫封装。
| public | protected | default | private | |
|---|---|---|---|---|
| 同一类中 | ok | ok | ok | ok |
| 同一包中 | ok | ok | ok | no |
| 子类中 | ok | ok | no | no |
| 不同包中 | ok | no | no | no |
不同包中能使用的;1、public 2、不同包中子类的protected
使用import,导入指定包中的类,import packa.;导入packa中的所有类,并不导入其中的包,如果packa下面还有包,则需要加入packa.abc.*;真实开发中不建议写
使用import可以导入包中的类,不用写包名.类名
导包的原则:用到哪个类就导入哪个类。
import作用:为了简化类名书写,导入的是包中的类。
Jar:java的压缩包,可以压缩
压缩命令:jar -cvf 压缩包名称.jar 被压缩包名
解压缩命令:jar -xvf 压缩包名称
将工具包打包为jar文件,不用解压缩,将jar文件添加到classpath下,然后变可以执行,利用java 包名.类名执行。
外部类与内部类修饰符
如果类可以使用private来修饰,表示该包下的这个类不能被其它类访问,那么该类也失去了存在的意义,所以不能使用private来修饰类。
如果类可以使用protected来修饰,表示该类所在的包的其它类可以访问该类;该类所在的包的子包的类可以访问该类,但是包没有继承的概念,所以后一句是不对。所以用protected来修饰类也是没有意义的。
外部类的修饰权限只能是public或者包访问权限,但是内部类可以是private或者protected。
如果没能为访问权限指定一个访问修饰符,它就会默认得到包访问权限,意味着该类的对象可以由包内任何其他类来创建,在包外是不行的。相同目录下所有不明确package声明的文件,都被视作是该目录下默认包的一部分。
多线程
进程:正在进行中的程序(直译)
线程:负责进程中程序执行的一个控制单元,也称为执行路径,执行情景。
一个进程中可以有多个执行路径,称之为多线程。
一个进程中至少要有一个线程。
开启多个线程是为了同时运行多部分代码;每一个线程都有自己运行的内容,这个内容可以称为线程要执行的内容。
多线程弊端:线程太多导致运行效率低。应用线程的执行都是CPU在做着快速的切换完成的,CPU切换是随机的。
JVM启动时启动了多个线程,至少有2个可以分析出来。
- 执行main函数的线程
该线程的任务代码都定义在main函数中。
- 负责垃圾回收的线程
每个对象都有finalize()方法,当垃圾回收器确定不存在该对象的更多引用时,由对象的垃圾回收器调用此方法。
线程的状态
sleep(time)时间到,线程便从冻结状态变为运行状态。如果使用wait()方法,使用notify()方法才能唤醒线程。
运行状态:具备执行资格,具备执行权
冻结状态:释放执行权的同时释放执行资格
临时阻塞状态:具备着执行资格但是不具备执行权
CPU的执行资格:可以被CPU处理,在处理队列中排队
CPU的执行权:正在被cpu处理
如果类已经有父类,要拓展此类的功能,让其中的内容可以作为线程的任务执行,通过接口的形式完成
创建线程方式一
方式一:继承Thread类
定义一个类继承Thread类
覆盖Thread类中的run()方法。Thread类用于描述线程,因此Thread类也有对任务的描述,这个任务就是通过Thread类中的run()方法来体现,因此 run()方法就是封装自定义线程运行任务的函数。run方法中定义的是线程要运行的任务代码。
直接创建Thread的子类对象创建线程
调用start()方法,作用为启动线程,调用run()方法
开启线程是为了运行指定代码,所以只有继承Thread类,并覆写run()方法,将运行的代码定义在run()方法中。
可以通过Thread的getName(0方法来获取线程的名称 Thread-编号(从0开始),线程一被创建就调用super(),被赋予编号。
Thread.currentThread().getName(),获取正在运行的线程名字,主线程的名字就是main。如果想对线程起名,可以用Thread(String name),用super(“名字”)父类构造器来起名字。
对于多线程的内存图解,相当于main函数中开启了多条通道
不同的线程有不同的工作区,每个放大的进栈弹栈在单独的空间内完成,每个方法中的变量互不冲突。
只要有一个前台进程还在运行,程序就不会结束。
在异常中,会显示异常出现的具体线程,在哪个线程出异常,就显示哪个线程。
创建线程方式二
方式二:实现Runnable接口
定义类实现Runnable接口
覆盖接口中的run()方法,将线程的任务代码封装到run()方法中
通过Thread类创建对象,并将Runnable接口的子类对象作为Thread类构造函数的参数进行传递
原因:因为线程的任务都封装在Runnable子类对象run()方法中,所以要在线程对象创建时明确要运行的任务。
调用线程对象的start()方法启动线程
实现Runnable接口,如果使用无参数的Thread构造方法,则run()方法不做动作,如果传入Runnable子类,则调用的是子类中的方法。如果使用继承Thread类,则此类将父类中的run()方法覆盖,也没有影响。
使用多线程的目的是讲线程任务进行执行,如果直接继承Thread,将所有的线程类中的方法继承没有必要;如果只是实现Runnable接口,则它的出现仅仅是将线程的任务进行了对象的封装。为了运行创建Thread对象并明确线程的任务。
Thread类实现Runnable接口原因,此类与其他线程类都有共性:线程方法,但是此方法不是必须的,而是额外的功能,因此被抽取成一个接口。
实现Runnable接口的好处
将线程的任务从线程的子类中分离出来,进行了单独的封装。按照面向对象的思想将任务封装成了对象。
避免了java单继承的局限性
所以创建线程的第二种方式较为常见
异常:所属线程+异常名称+异常信息+异常位置
卖票程序中,如果使用继承,新建多个Thread子类对象,每个对象都有票信息,没实现信息共享。需要利用实现Runnable接口,这样只产生一个对象,将线程任务封装成对象传递给Thread类,然后新建多个Thread类对象调用start()方法,因为只有一个Runnable子类对象,因此在堆内存中只存在一个票数信息。
卖票时候会出现,一个线程获取票数准备-1,此时切到另一个线程也获取票数,此时票数仍然符合要求,也准备-1,这样会出现票数<=0的情况,会出现线程安全问题。
线程安全问题产生原因
多个线程在操作共享的数据。
操作共享数据的线程代码有多条。(操作共享数据代码在2行以上容易出事)
当一个线程在执行操作共享数据的多条代码中,其他线程参与了运算,就会导致线程安全问题的产生。
解决思路:将多条操作共享数据的线程代码封装起来;当有线程在执行这些代码的时候,其他线程是不可以参与运算的。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
同步代码块
在java中用同步代码块可以解决这个问题。
格式为:
sysnchronized(对象){需要被同步的代码}
此对象不能每次新建 对象,不然不是同一个锁
同步时设置对象相当于一个标志位,所有线程要运行任务时候要先进行状态判断,如果有线程正在进行此任务则需要等待此线程任务执行完毕。后期对同步中的线程进行监视,线程要判断同步锁。
同步的好处:解决了线程的安全问题
同步的弊端:相对降低了效率,因为同步外的线程都会判断同步锁。
Thread.sleep()需要进行try,catch,如果此类实现了接口,则不能抛出错误,只能解决,接口的异常对象为InterruptedException
同步的前提:同步中必须有多个线程并使用同一个锁。
如果同步锁的对象是成员变量,一个对象堆中只有一个,可以同步;如果此对象在run()方法中定义,则每个线程栈中都有一个,则不能实现同步。
线程开启必须有任务
使用同步函数也能解决线程的安全问题,将函数中加上synchronized修饰符。同步函数使用的锁是this。
同步函数和同步代码块的区别:
同步函数的锁是固定的this,同步代码块的锁是任意的对象。建议使用同步代码块。
如果同步函数使用静态修饰,因为静态方法中没有this,所以此时同步代码块中的同步锁对象为该函数所属字节码文件对象,可以用getClass方法获取,也可以用类名.class表示。
获取字节码文件:
Class clazz = t.getClass();
Class clazz = Ticket.class;`
使用懒汉式,在第一次的时候进行实例化,如果多个线程进来,先进行s的判断,可能会初始多个对象,这时候就有多线程的安全隐患,将getInstance()方法加上synchronized修饰即可。但是这样每个线程进来都要判断同步锁,降低了效率,可以使用同步代码块,其中同步对象为Single.class。
线程0进来,此时s==null,开启同步锁,此时进入if判断,CPU切走,然后线程1进来,进入if判断,同步锁进不去,等线程0切回来,创建一个s对象,再切回到线程1,此时因为不满足if判断,因此结束。如果再有其他线程进来,不满足最外层s==null判断,因此也不会降低效率。
加一层同步,是为了解决线程安全问题;加一层判断,是为了解决效率问题。利用双重判断来解决懒汉式的安全和效率问题,因此开发式饿汉式更好。
死锁
常见情形一:同步的嵌套
设置两个状态,同步嵌套,线程A同步锁为a,b;线程B同步锁为b,a。如果两个线程进去时候一个持有锁a,另一个持有锁b,这样就会出错。
线程间通讯
线程间通讯:多个线程在处理同一资源,但是任务却不同。
需求:有一资源,有输入与输出的方法,要求一个输入就一个输出,这样便涉及到了多线程。
设计思路:将资源封装成对象,输出实现Runnable接口,设置循环输出资源的信息;输入实现Runnable方法,设置循环获得资源的信息。因为输入与输出为2个类,而且都要获得资源对象,如果两个分别new资源对象,这样资源没有实现共享。因此要让输入输出 操作同一个对象,可以使用单例设计,也可以采用传参的形式,对输入输出进行构造函数传入参数对象,进行参数的设置。在主函数中,先创建资源,然后创建任务,再创建线程,执行路径,最后开启线程。
实现0,1的切换,利用x%2,在输入端进行2种设置,利用x值的不同来切换,如果张三,男;李四,女。
在主函数中运行,会出现张三,女和李四,男的情况,这是因为输入在设置的时候,便进行了输出。如果直接使用同步,则无法解决,这时候要思考同步的前提:一个同步锁中是否有多个线程。不符合,因为输入线程中只有一个线程,输出线程不在同步中。如果两边同时加入同步,但是不使用同一个锁,因此还是无法解决。这时候考虑到资源对象是唯一的,使用资源对象作为锁。这样便解决了对象输出错误的问题,但是输出的结果不是一个男,一个女。是因为输入拿到资源,不会只赋值一次,便一直在赋值;输出也一样,不会只输出一次,因此一直输出最后一次赋值结果。需求的效果是一个输入完后就进行输出。
为了解决这个问题,应该在资源中加入标记,如果有数据,那么便不进行覆盖,如果没有数据,再进行输入。加入一个数据后,标志位改变,这时候要等待输出线程进行输出。
在输入中,如果标记位为false,则没有信息,应该写入信息,将flag置为true,唤醒output线程,如果flag为真(还有信息,没有输出),则冻结input线程。在输出中,如果标记位为true,则有信息,应该输出信息,将flag置为false,唤醒input线程,如果flag为假(没有信息,还没输入),则冻结output线程。
等待唤醒机制
涉及的方法:
wait();让线程处于冻结状态,被wait的线程会被存储到线程池中。
notify();唤醒线程池中的一个线程(任意)。
notifyAll();唤醒线程池中的所有线程。
这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法。必须要明确到底操作的是哪个锁上的线程。
其中wait()和notify()方法要有所属,应该是同步锁的对象.wait()和同步锁的对象.notify()。在哪个对象中被等待和唤醒,就用哪个线程唤醒和等待.
操作线程的方法wait,notify,notifyAll定义在Object类中原因是:因为这些方法是监视器的放法,监视器其实就是锁。锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。
其中wait()抛出了InterruptedException异常,要有try,catch。
然后利用面向对象的思想,将姓名性别设置为私有属性,设置专门的set和out方法来设置和获取其属性。而因为定义在了资源这一个类中,可以直接利用同步方法来实现同步,同步锁为this,然后加进去flag判断和x切换输入,转变flag并唤醒线程池中线程。其中输出直接调用out方法即可。
单生产不会出问题,但是多生产多消费出问题。
如果t0,t1是生产,t2,t3是消费,那么第一下t0进行生产了第1只烤鸭,此时烤鸭数为2,flag=true,这是t0进入了休眠状态,此时临时阻塞状态的线程有t1,t2,t3。这时候如果t1被CPU执行到,flag=true,t1也进入休眠状态。这时候剩下t2,t3线程。t2被CPU执行到,消费烤鸭1,flag=false,唤醒线程池中的线程,这时线程池中线程有t0和t1,这时候要随机唤醒安全锁中的线程,假设t0被唤醒,这时候t2仍然有执行权,因为flag=false,t2进入休眠,此时有执行权的线程有t0与t3。假设t3抢到了CPU的执行权,因为flag=false,这时候t3也进入等待,只有t0活着。线程唤醒后直接接着执行,不用重新判断flag,因此t0生产烤鸭2。count=3,这时候flag=true。线程池中有3个线程,t0,t2,t3。这时候如果t0被唤醒,活的有t0,t1。这时候如果t0又抢到了CPU运行权,因为flag=true,t0进入休眠。因为t1不用再判断flag,因此直接生产烤鸭3,count=4。到现在为止,生产了烤鸭1,2,3,但是只消费了烤鸭1,产生了安全问题,烤鸭2没有被消费到。
原因在线程醒来后没有重新判断flag标记。因为if只判断一次标记,因此将if更改为while,结束后还会判断条件,便可以解决烤鸭没有被消费到问题。但是会出现死锁的问题。当一个烤鸭被生产,flag=true,唤醒另一个生产线程,这时候活的有t0,t1,进入while判断,因为flag均会进入等待,便没有活的线程。
解决死锁:没有唤醒对方才导致死锁。不能指定唤醒的线程,但是可以唤醒所有的线程,这样本方的继续等待,对方的进行工作。这样可以使用notifyAll()方法。
while判断标记,解决了线程获取执行权后,是否要运行!原因:if判断标记只有一次,会导致不该运行的线程运行了,出现了数据错误的情况。
notifyAll()解决了本方线程一定会唤醒对方线程。原因:notify()只能唤醒1个线程,如果本方唤醒本方,没有意义。而且while判断标记+notify()会导致死锁。
仍然存在的问题:还会唤醒本方线程,而唤醒本方是没有意义的。这样便降低了效率。
锁对象
JDK1.5后。同步代码块对于锁的操作是隐式的。后期把锁封装成对象,获取锁与释放锁是锁对象最清楚,变成显示的。现在有lock()与unlock()来对锁进行操作。因此lock对synchronized进行了替代,代码进行改写。
之前使用synchronized依靠自定义的obj对象作为锁,现在可以利用自定义的锁对象,语法为Lock lock = new ReentrantLock();。如果代码中发生了异常,释放锁不能执行,因此释放锁一定要进行执行,放在finally中。
以前的锁是this.wait()与this.notify(),用哪个锁,就用哪个锁上的方法。因此要利用lock来使用wait()与notify()。以前如果锁是this,那么锁上只有1组方法。当锁变成对象,可以有多个锁,将监视器方法封装成Condition对象。可以将多个Condition挂在锁上。
Condition因素出Object监视器方法( wait , notify和notifyAll )成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock来实现。 Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。可以随时和锁进行绑定,通过lock.newCondition()获取一个Condition对象。
将原来的this.wait()替换成con.await(),将this.notify()替换成con.signal(),将this.notifyAll()替换成con.signalAll()。
之前只有1个监视器,对生产者消费者区分不开。因此现在可以有2个监视器,1组监视生产者,1组监视消费者(以前这么实现需要生产者与消费者分别有个锁,因为一个锁上只有1个监视器)。因此生产者上唤醒消费者,消费者上唤醒生产者。因此可以不用使用signalAll来避免死锁,只要唤醒相应的线程即可。
生产者如果不满足,则让生产者进行等待,生产者唤醒的时候要唤醒消费者的线程。即以前只有1个线程池,但是现在有2个监视器,具有不同的线程池。
Lock接口:出现替代了同步代码块或者同步函数。将同步的隐式锁操作变成显示锁操作。同时更为灵活,可以一个锁上挂多个监视器。
lock():获取锁
unlock():释放锁,通常需要定义在finally代码块中。
Condition接口:出现替代了Object中的wait notify notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以任意锁进行组合。
await():相当于wait()
signal():相当于notify()
signal()All:相当于notify()All
判断条件一定要用while(),因为安全。
使用流程:
上锁;
try{运行代码,判断条件使用while,满足条件,await,不满足继续进行。然后signal相应监视器}
finally{ 开锁 }
wait与sleep的区别
wait可以指定时间也可以不指定,sleep必须指定时间。
在同步中,对cpu的执行权和锁的处理不同。
wait():释放执行权,释放锁。
sleep():释放执行权,不释放锁。
不释放执行权,那么电脑就卡死了。
在同步中有执行资格不一定能执行,持有相应的锁才能执行。同步中只有一个线程能够执行,但是同步中活着的线程不一定只有一个。
停止线程
stop()方法,已过时
run()方法结束
如何控制线程的任务结束
任务中都有循环结构,只要控制住循环,就可以结束任务。
控制循环通常用定义标记来完成。while(flag),然后st.setFlag()来改变标识。但是如果线程在同步中wait()释放执行权与锁,这样便没办法去读标记。
线程处于冻结状态就无法读取标记,结束方法是:interrupt()方法,将线程从冻结状态强制恢复到运行状态中来,让线程具备CPU的执行资格,会抛出中断异常。强制执行会发生InterruptedException,记得要处理。
线程中常见方法
setDaemon(boolean on):将该线程标记为守护(后台)线程,当所有线程都是守护线程时,jvm退出运行,必须在 线程启动前设置。后台线程与前台线程只有结束时候不一样,前台线程要设置手动结束,后台线程只要所有前台线程结束,则后台线程自动结束。如果想让一个线程依赖于其他线程,则可以设置为后台线程。
join(),常用于临时加入一个线程运算时,先将此对象运行完,将调用此方法的线程冻结,只有在join的方法运行结束后才恢复。
setPriority(优先级1-10),设置线程运行优先级,1最低,10最高,默认为5。
Thread(ThreadGroup group) 设置线程组
yield()暂停当前线程,执行其他线程。礼让
线程与匿名内部类
1 | new Thread(){ |
开启一个匿名的线程子类对象。
1 | Runnable r = new Runnable(){ |
新建一个匿名的Runnable子类对象,然后新建线程对象并调用start()方法。
方法定义完整格式
Java中方法定义的完整格式
访问权限{ public | default | protected | private }[final] [static] [ synchronized]
返回值类型 | void 方法名称{参数类型 参数名称,…}[throws Exception1, Exception2]{
[return [返回值|返回调用处]]
}
使用对象
从面对对象的思想转变为使用对象
String类
字符串是一个特殊的对象,字符串一旦初始化就不可以被改变。
有字符串常量池,池中没有就建立,池中有,直接用。因为字符串对象不可改变,因此可以实现共享
字符串建立的方式
String s = “abc”; 创建一个字符串对象在常量池中
String s = new String(“abc”); 创建两个对象一个new一个字符串对象在堆内存中被new对象维护
String类中的equals覆写了Object中的equals建立了String类自己的判断字符串对象是否相同的依据,其实就是比较字符串内容。
将字节数组变成字符串,把数字进行ASII码的对应。
String的构造器,可以将数组变成字符串(byte[]与char[]),传入数字变成对应的字符
String(byte[] bytes);String(byte[] bytes, int offset, int length);定义起始位置,长度
String(char[] value); String(char[] value, int offset, int count)
寻找方法的思路:思考返回类型和传入类型
按照面向对象的思想对字符串功能进行分类
获取
1.1 获取字符串中字符的个数(长度)
1 | int length(); |
1.2 根据位置获取字符
1 | char charAt(int index) |
1.3 根据字符获取在字符串中第一次出现的位置(重点)我们可以根据-1,来判断该字符或者字符串是否存在
1 | int indexOf(int ch) |
根据字符获取在字符串中最后一次出现的位置
1 | int lastIndexOf(int ch) |
1.4 获取字符串中一部分字符串,也叫子串。包括begin,不包括end
1 | String substring(int beginIndex, int endIndex) 子串开始于指定beginIndex并延伸到字符索引endIndex-1 |
转换
2.1 将字符串变成字符串数组(字符串的切割)按照指定规则切割
1 | String[] split(String regex) ,不能直接用.切割,要转义成\\.,涉及到正则表达式 |
2.2 将字符串转成字符数组
1 | char[] toCharArray() |
2.3 将字符串转成字节数组
1 | byte[] getBytes() 一个中文2字节,ascii为美国的编码表,GB2312中文的编码表最大为1 |
2.4 将字符串中的字母转成大小写
1 | String toUpperCase() 大写 |
2.5 将字符串中的内容进行替换
1 | String replace(char oldChar, char newChar) 没替换成功,仍然返回原来的字符串 |
2.6 将字符串两端的空格去除
1 | String trim() |
2.7 将字符串进行连接
1 | String concat(String str) |
判断
3.1 两个字符串是否相同
1 | boolean equals(Object obj) |
3.2 字符串中是否包含指定字符串
1 | boolean contains(CharSequence s) |
3.3 字符串是否以指定字符串开头,是否以指定字符串结尾
1 | boolean startsWith(String prefix) |
比较
int compareTo(String anotherString) 如果参数字符串等于此字符串,则返回值0;如果此字符串按字典顺序小于字符串参数,则返回一个小于0的值;如果此字符串按字典顺序大于字符串参数,则返回一个大于0的值。
String intern()当调用intern方法时,如果池已经包含与[equals(Object)](mk:@MSITStore:D:\java学习资料\jdk api 1.8_google.CHM::/java/lang/String.html#equals-java.lang.Object-)方法确定的相当于此String对象的字符串,则返回来自池的字符串。 否则,此String对象将添加到池中,并返回对此String对象的引用。 由此可见,对于任何两个字符串s和t , s.intern() == t.intern()是true当且仅当s.equals(t)是true 。可以将堆中的数据存到字符串池中去。
StringBuffer类
StringBuffer:就是字符串缓冲区,用于存储数据的容器
特点:
长度是可变的
可以存储不同类型的数据
最终要转成字符串进行使用
可以对字符串进行修改
既然是一个容器对象,应该具备的功能有
添加
1
2StringBuffer append(data);可以继续在append后面继续添加
StringBuffer insert(int offset, char c) 在指定位置进行添加删除
1
2
3StringBuffer delete(int start, int end):包含头,不包含尾
StringBuffer deleteCharAt(int index) :删除指定位置的元素
sb.delete(0, sb.length());清空缓冲区查找
1
2
3char charAt(index);
int indexOf(String);
int lastIndexOf(String);修改
1
2
3StringBuffer replace(int start, int end, String str)
void setCharAt(int index, char ch) 不返回本类对象
void setLength(int newLength) 设置长度
增删改查C(create)U(update)R(read)D(delete)
可变长度数组原理,如果长度超出,则新建一个长度与原数组相同的数组,然后将两个数组长度相加,原有的数据复制。
StringBuilder类
jdk1.5以后出现了功能和StringBuffer一模一样的对象,就是StringBuilder。
不同之处,
StringBuilder是线程同步的,通常用于多线程
StringBuffer是线程不同步的。通常用于单线程,它的出现提高效率
StringBuilder因为没有判断锁,因此单线程使用StringBuilder,如果多线程使用StringBuffer。如果操作全部是字符串,那么就用StringBuilder
jdk升级原则
简化书写(可能有弊端)
提高效率(可能有弊端)
提高安全性(书写麻烦)
基本数据类型
8种:byte,int,short,long,double,float,char,Boolean
将基本数据类型包装成类
用基本数值类型和字符串做转换
基本数据类型包装类
为了方便操作数据类型值,将其封装成了对象,在对象中定义了属性和行为,丰富了该数据的操作
用于描述该对象的类就称为基本数据类型包装类
byte Byte
short Short
int Integer 静态方法parseInt(String s)
long Long
float Float
double Double
char Character
boolean Boolean
该包装对象主要用于基本类型和字符串之间的转换
基本类型—>字符串
基本数值类型+””
用String类中的静态方法valueOf(基本数值类型)
用Integer的静态方法toString(int i, int radix)
字符串—>基本类型
使用包装类中的静态方法
1
2
3
4xxx parseXxx("xxx类型的字符串");***最常用
int parseInt("intString");Integer.parseInt()
long parseLong("longString");Long.parseInt()
boolean parseBoolean("booleanString"); Boolean.parseBoolean()如果字符串被Integer进行对象的封装。可使用另一个非静态的方法:intValue()
将一个Integer对象转成基本数据类型值
Integer j = new Integer(“123”);
System.out.println(j.intValue());
进制转换
- 十进制–>其他进制
1 | static String Integer.toBinaryString(int i);//2进制 |
- 其他进制–>十进制
1 | static int parseInt(String s, int radix) |
Integer中比较的是数值是否相同。如果用equals方法就是返回boolean,如果用compareTo方法返回的是数值,可以知道哪个比较大。
1 | public Integer(int value) |
而因为i为对象,因此i可能为null,如果空的调用intvalue()方法会出错,因此在自动拆箱的时候要进行健壮性判断。
集合框架
集合类的由来
对象用于封装特有数据,对象多了就需要存储,如果对象的个数不确定,
就使用集合容器进行存储
集合特点:
用于存储对象的容器
集合的长度是可变的
集合中不可以存储基本数据类型
集合容器因为内部的数据结构不同,有多种具体容器。
不断向上抽取,就形成了集合框架
框架的顶层Collection接口:
Collection的常见方法:
- 添加
1 | boolean add(Object obj);添加一个对象 |
- 删除
1 | boolean remove(Object obj);删除一个对象,利用equals方法判断 |
判断
1
2
3boolean contains(Object obj);判断是否有此对象,利用equals方法判断
boolean containsAll(Collection coll);判断是否有一堆对象
boolean isEmpty();判断集合中是否有元素获取
1 | int size();获取长度 |
该对象必须依赖于具体的容器,因为每一个容器的数据结构都不同。
所以该迭代器对象是在容器中进行内部实现的。
对于使用容器者而言,具体的实现不重要,只要通过容器获取到该实现的迭代器对象即可,也就是iterator方法。
- 其他
1 | boolean retainAll(Collection coll);取交集,取交集,保留和指定集合相同的元素,而删除不同的元素。与removeAll相反 |
集合判断元素依据
对于ArrayList这样的集合,判断元素的方式是依据equals方法,而对于HashSet这样的集合,依靠的是equals和hashCode方法
迭代器
取出元素的两种方法
Collection coll = new ArrayList();
使用while循环,结束后it占用内存空间,开发中不推荐
1
2
3Iterator it = coll.iterator();
while(it.hasNext())
System.out.println(it.next());使用for循环
1
2
3for(Iterator it = coll.iterator();it.hasNext();) {
System.out.println(it.next());
}
iterator是一个内部类。类比于娃娃机的夹子,娃娃机是容器,夹子是迭代器,迭代器不能直接new,只能通过容器来操作夹子,但是统一的有移动和按钮。
集成框架的构成
常见集合关系图
虚线框都是接口
Collection
|--List: 有序,(存入和存出的数据一致),元素都有索引(角标),元素可以重复。 |–Set: 元素不能重复,无序。
List
List特有的常见方法:有一个共性特点就是都可以操作角标
添加
1
2void add(int index, E element);
void add(index,collection);删除
1
Object remove(index);
修改
1
Object set(index,element);
获取
1
2
3
4Object get(index);
int indexOf(Object);
int lastIndexOf(Object);
List subList(from,to);包含头不包含尾List集合是可以完成对元素的增删改查
1 | //一般集合的取出方式 |
当迭代器与集合同时操作元素,就会产生异常,这样是并发操作。
在迭代过程中不要使用集合操作元素,容易出现异常
可以使用Iterator接口的子接口ListIterator来完成迭代中对元素进行更多的操作
1 | ListIterator it = list.listIterator();//获取列表迭代器对象 |
ListIterator listIterator()从头返回列表迭代器
ListIterator listIterator(int index)从指定位置返回列表迭代器
不仅有hasNext()和next(),还有hasPrevious()和previous()方法
List接口中比较常用的有:ArrayList,LinkList。
List
|–Vector:内部是数组数据结构,是同步的。效率低。增删、查询都很慢!
|–ArrayList:内部是数组数据接口,是不同步的。替代了Vector。查询的速度快。
|–LinkedList:内部是链表数据结构,是不同步的。增删元素的速度很快。
数组增删慢的原因是增加删除一个元素,该元素之后的内存空间均要发生变化,而查询快是因为内存空间是连续的。而链表存储是不连续的,而链表的增删只需要将此对象的地址存给上一个对象或者将此对象所存的下一个对象的地址存入上一个对象即可完成。查询慢是因为要遍历链表才能完成查询,而链表存储空间不是连续的(作为List的子类也有下标索引)。
{% asset_img 第16天2.png This is an example image %}LinkedList
1 | void addFirst(element) 在链表的开头加入指定的元素 |
遍历链表的一种方式
1 | while(!link.isEmpty()){//判断是否为空,为空则为真 |
Linkedist常用方法与1.6改进
添加
addFirst();
addLast();
jdk1.6
offerFirst();
offerLast();
获取
getFirst();//获取但不移除。如果链表为空,抛出NoSuchElementException
getLast();jdk1.6
peekFirst();//获取但不移除。如果链表为空,返回null
peekLast();
移除
removeFirst();//获取并移除。如果链表为空,抛出NoSuchElementException
removeLast();
jdk1.6
pollFirst();//获取并移除。如果链表为空,返回null
pollLast();
ArrayList
默认新建ArrayList对象的时候,开辟的空间为10。
((Person)it.next()).getName()+”::”+((Person) it.next()).getAge());
//不能直接调用Person的方法,因为此时被提升为了Object,父类中没有此方法,可以使用强制类型转换。如果不给it.next加括号, 那么因为.优先级太高而没有先完成强制转换。
如果在循环中多次调用it.next,那么会出错,因为next一直在往下走。
因此如果使用自定义对象,一直要使用强制类型转换,才能获取该对象的特有方法。直接打印it.next(),出现的是类名加哈希值,因为该对象没有覆写toString()方法。
内存图解,集合中存储的均为堆内存对象的引用,迭代器取出元素,也是使用的引用。
1 | ArrayList al = new ArrayList(); |
当基本数据类型赋值给引用数据类型的时候,会发生自动装箱。因为集合中需要添加对象,直接用数字相当于对5进行了Integer装箱。当引用数据类型与基本数据类型做运算,会发生自动拆箱。
Vector
枚举
使用element方法获取枚举。
Vector中有枚举接口Enumeration,功能与Iterator一样,但是Iterator名称更简单。
Set接口
Set:元素不可重复,是无序。
Set接口中的方法和Collection一致。
|–HashSet:内部数据结构是哈希表,是不同步的。
保证集合元素的唯一性
是通过对象的hashCode和equals方法来完成对象的唯一性的。
如果对象的hashCode值不同,那么不用判断equals方法,就直接存储到哈希表中。
如果对象的hashCode值相同,那么要再次判断对象的equals方法是否为true。
如果为true,视为相同元素,不存。如果 为false,视为不同元素,进行存储。
**记住:如果元素要存储到HashSet集合中,必须覆盖hashCode方法和equals方法**。 一般情况下,如果定义的类会产生很多对象,比如学生,人,书,通常都要覆盖equals,hashCode方法。 建立对象判断是否相同的依据。
|–TreeSet:可以对Set集合中的元素排序。是不同步的。
判断元素唯一性的方式:就是根据比较方法的返回结果是否为0,是0,就是相同元素,不存。
TreeSet对元素进行排序的**方式一**:让元素自身具备比较功能,元素就需要实现Comparable接口,覆盖compareTo方法。
如果不要按照对象中具备的自然顺序排序。如果对象中不具备自然顺序。怎么办?
TreeSet对元素进行排序的方式二:
让集合自身具备比较功能。定义一个类实现Comparator接口,覆盖compare方法。
将该类对象作为参数传递给TreeSet集合的构造函数。
比较器发开中更为常用,如果有比较器和类自己的compareTo方法,优先使用比较器。
HashSet
优化过的数组,根据存的元素的特点来获取其数组中的位置。查找的时候,根据这个元素再计算其对应的位置值,然后直接取出即可。算法为哈希算法。hashCode()为计算对象的哈希值,自定义对象可以覆盖方法。
{% asset_img 第17天2.png This is an example image %}如自定义哈希算法,根据ab的特点算出一个值,再将值%数组长度,必定会得到数组长度之内的一个值,这样就可以获取到ab的位置。当要寻找ab的时候,再计算ab的哈希值,这样直接在相应的位置去寻找即可。
Set的获取只能使用迭代器。
哈希算法提高了数组的查询效率,但是不能重复。
哈希表确定元素是否相同
判断的是两个元素的哈希值是否相同。如果相同,再判断两个对象的内容是否相同。
判断哈希值相同,其实判断的是对象的hashCode()方法。判断内容相同,用的是equals()方法。
注意:如果哈希值不同,是不需要判断equals的。
哈希值一样,但是对象不一样,称之为哈希冲突。如果冲突则顺延或者串联。在相同位置挂一个出来。
当存入的是自定义对象时候,需要覆写hashCode方法和equals方法。如果是字符串则调用字符串的hashCode(),如果是数字,则乘上一个常数。进行比较的时候,首先判断位置是否相同,然后向下转型之前进行健壮性判断,然后比较对应的信息。为了便于输出信息,可以覆写对象的toString方法。
1 |
|
哈希集合中判断元素是否相同的依据是hashCode方法和equals方法。
LinkedHashSet
哈希表和链表实现了Set接口,具有可预测的迭代次序。 这种实现不同于HashSet,它维持于所有条目的运行双向链表。 该链表定义了迭代排序,它是将元素插入集合(插入顺序 ) 的顺序 。
怎么样存就怎么取出来。
如果要唯一而且要有序,就用LinkedHashSet。
TreeSet
如果要存入自定义对象,需要此对象实现Comparable接口并覆盖compareTo方法。
二叉树图解
{% asset_img 第17天3.png This is an example image %} 28进来不用比较,21进来比28小放在左边,29进来比28大放在右边。25比28小进左边,比21大生叉放在21右边,以此类推。
节点的特点:最多持有3个引用,左、右、父。二叉树为了提高效率,会在每次添加新元素时候进行二分查找。
泛型
泛型:
jdk1.5后出现的安全机制。
好处:
将运行时期的问题ClassCastException转到了编译时期。
避免了强制转换的麻烦。
<>:什么时候用?当操作的引用数据类型不确定的时候,就使用<>,将要操作的引用数据类型传入即可。
其实<>就是一个用于接收具体引用数据类型的参数范围。
在程序中,只要用到了带有<>的类或者接口,就要明确传入的具体引用数据类型。
泛型技术是给编译期使用的技术,用于编译时期。确保了类型的安全。
运行时,会将泛型去掉,生成的class文件中是不带泛型的,这个称为泛型的擦除。
擦除的原因:为了去兼容运行时的类加载器。
泛型的补偿:在运行时,通过获取元素的类型进行转换动作,不用使用者再强制
泛型中不能使用基本数据类型,只能传入引用数据类型。
Comparable接口上定义了泛型,指定进行比较的参数类型。
1 | public class Person implements Comparable<Person>{ |
如果不指定,默认为Object,而且在compareTo方法中还要向下转型。
泛型类
在jdk1.5后,使用泛型来接收类中要操作的引用数据类型。
泛型类,什么时候用?当类中的操作的引用数据类型不确定的时候,就使用泛型来表示。
1 | public class Tool<Q>{ |
泛型方法
1 | public |
将泛型定义在方法上,W为类型名称,待传入,而W要进行声明。
当方法静态时,不能访问类上定义的泛型,如果静态方法使用泛型,只能将泛型定义在方法上。
泛型一定要放在返回值的前面,修饰符的后面
1 | public static |
如果使用泛型,则有些对象才有的方法则不能使用,但是部分方法可以使用,即Object中的方法必定可以使用
泛型接口
实现一:
1 | class InterImp implements Inter<String>{ |
主函数中
1 | InterImp in = new InterImp(); |
实现二:
1 | class InterImp2<Q> implements Inter<Q>{ |
主函数中
1 | InterImp2 |
泛型通配符
泛型的通配符:?未知类型。
当不能明确传入泛型的类型时候使用。仅在类型不明确并不对这类型操作来表示。
1 | public static void printCollection(Collection al) { |
也可以使用
1 | public staticvoid printCollection(Collectional){ |
但是比较麻烦
泛型限定
只能有一部分类型能进来
可以对类型进行限定
? extends E:接收E类型或者E的子类型对象。上限!
? super E:接收E类型或者E的父类型,下限!
迭代器的泛型和获取迭代器集合的泛型一致。
泛型上限:
如果有不同的集合,分别创建Person类的子类,一个方法要接收Person类子类的集合,不能直接接收Collection
一般两边泛型类型要一致。这样要如果限定只能接收某一对象的子类,可以使用
,即只接收Person或者Person的子类。这样可以直接新建Person类对象。
泛型下限:
使用,只能接收学生类及其父类
addAll(Collection e)原因,可以接收该集合类型及子类类型,提高了拓展性和安全性。一般只要写了E就可以接收该类的集合对象,但是为了能接收其子类对象,增加了泛型的上限。
使用情况:
1 | addAll(Collection e) |
一般存储元素的时候都使用上限,因为这样取出都是按照上限类型来运算的,不会出现类型安全隐患。
1 | TreeSet(Comparatorsuper E> comparator) |
学生的集合中,比较器可以用学生的也可以用父类的,因为其使用的方法源于父类。
通常对集合中的元素取出操作时,可以使用下限。存什么类型,我可以用其类型和父类类型来进行接收。
包含containsAll(Collectioncoll)为什么用?
因为Collection的原理为equals,任何对象都具有其方法,因此接收的集合类型可以是任意类型,因此用?来进行接收。只要全部使用Object方法,因此就可以使用?。
集合框架查询技巧
需要唯一吗?
需要:Set
需要指定顺序吗?
需要:TreeSet
不需要:HashSet
但是想要一个和存储一致的顺序(有序):LinkedHashSet
不需要:List
需要频繁增删吗?
需要:LinkedList
不需要:ArrayList
如何记住每一个容器的结构和所属体系呢?
看名字!
List:
|–ArrayList
|–LinkedList
Set:
|–HashSet
|–TreeSet
后缀名就是该集合所属的体系。
前缀名就是该集合的数据结构。
看到arrays:就要想到数组,就要想到查询快,有角标。
看到link:就要想到链表,就要想到增删快,就要想到add get remove+first last的方法
看到hash:就要想到哈希表,就要想到唯一性,就要想到元素需要覆盖hashCode方法和equals方法。
看到tree:就要想到二叉树,就要想到排序,就要想到两个 接口Comparable,Comparator(比较器)
而且这些通常这些常用的集合容器都是不同步的。
Map
Map:一次添加一对元素。Collection:一次添加一个元素。
Map也称为双列集合,Collection称为单列集合。
其实Map集合中存储的是键值对。Map集合中必须保证键的唯一性。
常用方法:
添加
1
value put(key,value);返回前一个和key关联的值,如果没有返回null
删除
1
2void clear();清空map集合
value remove(key);根据指定的key删除这个键值对判断
1
2
3boolean containsKey(key);
boolean containsValue(value);
boolean isEmpty();获取
1
2
3value get(key);通过键获取值,如果没有该键,返回null。
当然可以通过返回null,来判断是否包含指定键。
int size();获取键值对的个数。
取出Map中所有元素
方式一:取出丈夫,再取出妻子
1 | Map |
首先利用keySet()方法将Map中的键映射到Set集合中,然后通过迭代器可以得到每一个键key,再利用Map的get(key)方法得到每一个键对应的值。
其中被注释的两句相当于下面一句。
1 | // Set |
方式二:得到结婚证书
1 | /* |
通过entrySet()方法将键值的映射投射到Set集合中,其中将键和值封装成一个对象。
{% asset_img 第18天2.png This is an example image %}其中上面两句相当于下面一句。
1 | // Set |
只获取value值
Collection values();返回value所有值的Collection集合
Map常用的子类
|–Hashtable:内部结构是哈希表,是同步的。不允许null作为键,不允许null作为值。
|–Properties:用来存储键值对型的配置文件的信息。可以和IO技术相结合。
|–HashMap:内部结构是哈希表,不是同步的。允许null作为键,允许null作为值。
|–TreeMap:内部结构是二叉树,不是同步的。可以对Map集合中的键进行排序。
HashSet是HashMap的一个实例。
LinkedHashMap
可以将存储的元素有序输出,即存入什么顺序,取出什么顺序。
Map应用
Map集合在有映射关系时可以优先考虑
键值关系多,往Map里面存储,不一定需要有序编号,只是建立对象间的关系。
Map中的值也可以是集合,如List,Set
在查表法中的应用较为多见
集合框架工具类
Collections类
Collections:是集合框架的工具类,里面的方法都是静态的,操作集合。
1、 排序
自然顺序排序
Collections sort(List)
如果只对一种类型进行排序,那么就是
1 | public static void mySort(List |
如果想对任意类型的集合进行排序,那么需要使用泛型,可以使用或
public static
但是排序中用到了compareTo方法,只有实现了此接口才具有,因此对T进行限定,必须是Comparable接口的子类,则变成
public static
而Comparable也要指出泛型的类型,可以使用T。为了提高拓展性,为了可以让T的父类也能接收T,因此Comparable的泛型使用,即可以使用T或者T的父类进行接收,因此最终变成
public static
指定顺序排序
static
可以使用自定期的比较器进行排序,泛型使用是为了让子类能使用父类的比较器,因为可以用父类对象来接收子类。
static
sort还可以利用比较器进行排序。原理是增加一个比较器,然后比较的方法使用比较器中的compare方法,获取元素利用List集合中的get()方法。这时候就不要求List具备比较方法,为了可以用父类对象接收被比较元素,使用。
1 | public static |
1.1交换顺序
static void swap(List list, int i, int j)
1.2折半查找
需要先对元素进行自然排序或者比较器排序后才能使用
static
如果返回-2,负数表示没有找到,-2为-1减去1,意思是如果插入进去要在角标1处插入元素。减一的目的是避免插入点为0时候不知道是没有找到还是这个点在角标为0的地方。
可以按照比较器排序然后索引。
public static
Comparatorsuper T> c)
2、 求最大值
static
也可以使用比较器进行取最大值。
static
3、 比较器反向
如果要将一个集合中的元素反向输出,使用TreeSet实现,要加载比较器重新实现,原理如下,其实就是将o1,o2进行互换。
TreeSet
//匿名内部类
@Override
public int compare(String o1, String o2) {
int temp = o2.compareTo(o1);
return temp;
}
});
这样就可以实现反向输出,但是比较麻烦
static
static
实际演示:
TreeSet
如果自己有比较器,就将比较器输入进去
TreeSet
反转集合中的元素
static void reverse(List list)
4、 初始化集合
用指定元素替代集合中所有元素。
static
5、 随机集合元素
使用默认的随机源随机排列指定的列表
static void shuffle(List list)
使用指定的随机源随机排列指定的列表
static void shuffle(List list, Random rnd)
就是扑克牌的洗牌。或者掷骰子,只取第一个即可。
6、 枚举的转换
返回指定集合的枚举
static
返回一个数组列表,其中包含由枚举返回的顺序由指定的枚举返回的元素。
static
7、 同步!!!!重点
如果要在多线程中使用集合,则自己在集合中加锁,Collections工具类中提供有synchronizedxxx方法,如:
static
返回由指定集合支持的同步(线程安全)集合。
static
返回由指定列表支持的同步(线程安全)列表。
static
返回由指定地图支持的同步(线程安全)映射。
具体的实现原理为
给非同步的集合加锁
List list = new ArrayList();//非同步的
list = MyCollections.synList(list);//返回一个同步的List
1 | class MyCollections{//将非同步集合变为同步集合 |
只是加锁,只是加锁,只是加锁,具体实现方式还是调用其自身的方式。
8、 转数组
集合转成数组
1 | 使用的就是Collection接口中的toArray()方法 |
Arrays
操作数组的工具类
Arrays:集合框架的工具类。里面的方法都是静态的。
1、 二分查找
static int binarySearch(byte[] a, byte key)
static int binarySearch(byte[] a, int fromIndex, int toIndex, byte key)
基本数据类型数组大多都能二分查找,除了boolean
2、 复制
复制全部长度的数组
static boolean[] copyOf(boolean[] original, int newLength)
复制指定范围的数组
static boolean[] copyOfRange(boolean[] original, int from, int to)
8种基本数据类型数组都能复制
3、 比较
比较两个数组彼此是否相同,需要传入两个参数,不是Object中的比较。如果两个数组都包含相同数量的元素,则两个数组被认为是相等的,并且两个数组中所有对应的元素对都相等。 换句话说,如果两个数组以相同的顺序包含相同的元素,则它们是相等的。 另外,如果两者都是null ,则两个数组引用被认为是相等的 。
static boolean equals(char[] a, char[] a2)
深度比较,不仅比较数组中的对象,还比较对象中的内容
static boolean deepEquals(Object[] a1, Object[] a2)
4、 替换
将数组中的全部元素替换为指定元素
static void fill(char[] a, char val)
将数组中指定范围的元素替换为指定元素
static void fill(char[] a, int fromIndex, int toIndex, char val)
5、 排序
static void sort(char[] a)
对指定范围的数组进行排序
static void sort(char[] a, int fromIndex, int toIndex)
对对象进行自然顺序的排序
static void sort(Object[] a)
对泛型数组进行排序
static
6、 toString
返回指定数组的字符串表现形式,如果想直接输出字符串,就调用toString
1 | static String toString(char[] a) |
7、 转成集合
将数组转成List集合
该方法作为基于数组和基于集合的API之间的桥梁,与Collection.toArray()相[结合](mk:@MSITStore:D:\java学习资料\jdk api 1.8_google.CHM::/java/util/Collection.html#toArray–) 。
public static
重点:List asList(数组)将数组转成集合。
好处:可以使用集合的方法操作数组的元素。
注意:数组的长度是固定的,所以 对于集合的增删方法是不可以使用的。如add,remove,clear。否则会发生UnsupportedOperationException
可以使用contains,indexOf,indexOfLast,set等方法,只要不改变数组的长度即可
1 | /* |
jdk1.5新特性
foreach语句
简化书写,底层利用的迭代器
1 | /* |
遍历数组
1 | int[] arr = {1,3,5,2,1}; |
遍历Map集合,要将map转成单列的Set
1 | //可以使用高级for遍历map集合吗?不能直接用,但是可以将map转成单列的set,就可以用了。 |
可变参数
1 | /* |
如果使用普通的数组接收,流程为先创建数组,然后将数组传入要调用的方法,即
int[] arr = {1,4,2,1};
int sum3 = add(arr);
而如果使用可变参数,则变成
int sum4 = newAdd(5,1,3,2,1);
简化了书写,但是可变参数不能放在第一个(有两个以上变量时),而数组可以。
静态导入
import static java.util.Collections.sort;//静态导入,其实导入的是类中的静态成员
以前要写Collections.sort();
现在写sort();
System类
不能被实例化,方法通常为静态的。
out:“标准”输出流
in:“标准”输入流
1、 获取系统时间
1 | long l1 = 1549696253272l; |
2、 获取系统信息
1 | //获取系统的属性信息,并存储到Properties集合中,键和值都是字符串类型 |
跨平台使用技巧
不同平台一些符号不一样,在jvm加载的时候会获取系统信息,因此可以将这些不同平台的符号定义为一个全局常量,然后根据系统信息获取
//获取换行符号
private static final String LINE_SEPERATOR = System.getProperty(“line.separator”);
也可以自己给系统设置属性信息。
Runtime类(单例设计)
应用程序不能创建自己的Runtime类实例,可以 通过getRuntime()方法获取当前运行时。单例设计的模式。
开启一个程序进程
1 | /* |
杀死子进程
只能杀死由Runtime r开启的进程。
1 | Process p = r.exec("notepad.exe"); |
Math类
PI : double类型
1 | /* |
Random类
获取随机数还可以使用Random对象
1 | Random r = new Random(); |
Date类
在类的所有方法Date接受或返回年,月,日,小时,分钟和秒值,以下表述中使用:
y年代表整数y - 1900 。
一个月由0到11的整数表示; 0是1月,1是2月,等等; 11月12日。
日期(月的一天)以通常的方式从1到31的整数表示。
一小时由0到23之间的整数表示。因此,从午夜到凌晨1点的时间是小时0,从中午到下午1点的小时是12小时。
分钟一般以0〜59的整数表示。
秒由0到61的整数表示; 值60和61仅发生在闰秒上,甚至仅在实际上正确跟踪闰秒的Java实现中发生。 由于目前引入闰秒的方式,在同一分钟内不会发生两个闰秒,但是本规范遵循ISO C的日期和时间约定。
构造器
Date()
Date(long mills)
1 | Date date = new Date();//将当前日期和时间封装成Date对象 |
方法
比较两个日期
int compareTo(Date anotherDate)
测试此日期是否在指定日期之后。
boolean after(Date when)
测试此日期是否在指定日期之前。
boolean before(Date when)
日期与毫秒转换
1 | /** |
DateFormat类
对日期进行格式化,不能直接创建对象,需要使用DateFormat.getInstance(),在其中加入其字段,可以使用规定的格式,如默认的,LONG等。如果想要使用自定义的格式,就需要new其子类,SimpleDateFormat。
对日期对象进行格式化
工厂:生产对象的地方
将日期对象—->日期格式的字符串
返回String,使用DateFormat方法(或其子类),接收Date对象
使用的是DateFormat类中的format方法
即DateFormat.getDateInstance()
1 | Date date = new Date(); |
自定义日期时间格式
{% asset_img 第20天1.png This is an example image %}范例:在指定格式的时候使用字符串,具体的规则见上表。
1 | //风格是自定义的的解决方式,使用DateFormat的子类 |
Calendar类
通过Calendar.getInstance来获取其对象
使用的是键值对
常见方法
1、 get()获取时间
2、 add()指定时间的偏移
3、 set()设置时间
日期显示
使用get方法,其中月份、星期要进行转换。
1 | public class CalendarDemo { |
日期设置
1 | //设置指定日期 |
应用
可以先设置时间,然后再利用add进行日期偏移,可以知道每一年哪个月有多少天,即设置那一年,然后将set设置为目标月的下一个月,然后偏移量为日期-1即可,再获取天数即可以。注意月份是从0开始!!!
1 | public static void showDays(int year) { |
如果要获取昨天的时分秒,直接将日期偏移量设置为日期偏移-1即可。
IO流
记得关流,除非是System.in或者System.out
IO流用来处理设备之间的数据传输,Java对数据的传输通过流的方式,Java用于操作流的的对象都在IO包中。
流按操作数据分为两种:字节流与字符流。流按流向分为:输入流(将硬盘中的数据读入内存),输出流(将内存中的数据写入硬盘)。
输入流与输出流
相对于内存设备而言。
将外设中的数据读取到内存中:输入。
将内存中的数据写入到外设中:输出。
构造函数可以接收文件对象,也可以接收字符串,接收文件对象使用更多。
字符流的由来
其实就是:字节流读取文字字节数据后,不直接操作而是先查指定的编码表。获取对应的文字。再对这个文字进行操作。
IO流基类
字节流的两个顶层父类:
1,InputStream 2,OutputStream
字符流的两个顶层父类
1,Reader 2,Writer
这些体系的子类都以父类名作为后缀。
而子类名的前缀就是该对象的功能。
从熟悉的文字开始字符流。
FileWriter
操作文本文件对象
构造函数中加入true表示续写。
//需求:将一些文字存储到硬盘一个文件中。
记住:如果要操作文字数据,建议优先考虑字符流。
而且要将数据从内存写到硬盘上,要使用字符流中的输出流:Writer
硬盘的数据基本体现是文件,希望可以找到一个可以操作文件的Writer
找到了FileWriter
1 | private static final String LINE_SEPARATOR = System.getProperty("line.separator"); |
IO异常处理
因为流对象一般会抛出异常,因此一般在try外面创建对象,在try里面创建具体的引用。
因为关闭一定要执行,因此close放在finally中,其中close也需要进行处理。为了不出现空指针异常,需要去判断fw,只有在不为空的时候,才进行流关闭操作。
即变量定外面,new在里面,finally close在里面,不要忘记判断null。
1 | FileWriter fw = null;// 流对象在try外面进行创建 |
FileReader
读取方式1:
1 | //需求:读取一个文本文件,将读取到的字符打印到控制台。 |
读取时候如果读到了末尾,会使用-1标识符,如果继续读则还是-1。
读取方式2:
1 | FileReader fr = new FileReader("demo.txt"); |
这种读取方式,由于后两次读的数量不一样,因此会对第一次读取的数组进行覆盖。
复制文件一
读取一个便写一个
1 | /* |
复制文件二
使用缓冲来读取
1 | public class CopyTextTest_2 { |
复制文件图解
{% asset_img 第21天1.png This is an example image %}待读取的文件为源,使用流来读取,对应代码为FileReader fr = new FileReader(“demo.txt”),要写入文件,使用流来写入,对应代码为FileWriter fw = new FileWriter (“test.txt”)。而两个流之间没有直接关系,需要使用缓冲区来作为中转,为了将读入流与缓冲区关联,使用fr.read(buf);为了将写出流与缓冲区关联,使用fw.write(buf,0,len)。为了将流中的文件写出到输出源中,要使用fw.flush或者fw.close,flush可以多次刷新,而close只能使用一次。
字符流缓冲区
对应类:
BufferedWriter
BufferedReader
缓冲区的出现提高了对数据的读写效率。缓冲区要结合流才可以使用。在流的基础上对流的功能进行了增强。
进缓冲区写入后一定要刷新!!!
字符流缓冲区
BufferedWriter
特有方法:newLine()换行写入
写完记得要刷新flush()
BufferedReader
特有方法:readLine()按行读入
判断标志不为-1,而是null。使用String类型接收read。
写入
1 | private static final String LINE_SEPARATOR = System.getProperty("line.separator"); |
读取
1 | FileReader fr = new FileReader("buf.txt"); |
读取中缓冲区的read方法对父类方法进行了覆写
{% asset_img 第21天2.png This is an example image %}readLine原理:缓冲区从磁盘中使用父类read(buf)方法拿出数据到内存中,从内存中取出字符比较快,使用的是覆盖过的read方法,将取出的数据放在临时容器中,根据文本的行特点,判断是否为换行符,临时容器中存储的是一行的数据,不包含换行符,然后转换成字符串进行输出。
readLine方法使用了读取缓冲区的read方法,将读取到的字符进行缓冲并判断换行标记,将标记前的缓存数据变成字符串返回。
临时容器可以使用StringBuilder。因为最终返回的是字符串。
按行读取
1 | String line = null; |
作用:可以按行来读取,因为返回的为String类型的变量,就可以使用String中的方法,如判断该行是否包含某一字符串。
复制文件三
使用缓冲区来实现文件的读写
1 | FileReader fr = new FileReader("buf.txt"); |
复制文件四
使用缓冲区按行读取,为了换行使用newLine,一定要flush!!!
1 | FileReader fr = new FileReader("buf.txt"); |
装饰设计模式
装饰设置模式:
对一组对象的功能进行增强时,就可以使用该模式进行问题的解决。
装饰和继承都能实现一样的特点,进行功能的拓展增强。
区别:
首先有一个继承体系
Writer
|--TextWriter:用于操作文本
|--MediaWriter:用于操作媒体想到对操作的动作进行效率的提高。按照面向对象,可以通过继承对具体的进行功能的拓展。
效率的提高需要加入缓冲技术
Writer
|--TextWriter:用于操作文本 |–BufferTextWriter:加入了缓冲技术的操作文本的对象
|--MediaWriter:用于操作媒体
|--BufferMediaWriter:加入了缓冲技术的操作媒体的对象到这里就可以了,但这样做存在问题。
如果这个体系进行功能拓展,又多了流对象。那么这个流要提高效率是否也要产生子类?
是,会发现只为提高功能进行的继承,导致继承体系越来越臃肿。不够灵活。
重新思考问题
既然加入的都是同一种技术–缓冲。
前一种是让缓冲和具体的对象相结合。可以将缓冲进行单独的封装,哪个对象需要缓冲, 就将哪个对象和缓冲关联。
1 | class Buffer{ |
太麻烦,直接操作父类,利用多态.但是要使用Writer方法,继承即可。
1 | class BufferWriter extends Writer{ |
Writer
|--TextWriter:用于操作文本
|--MediaWriter:用于操作媒体
|--BufferWriter:用于提高效率装饰比继承灵活。
特点:装饰类和被装饰类都必须所属同一个接口或者父类。
LineNumber
获取行号
1 | public static void main(String[] args) throws IOException { |
字节流
基本操作与字符流相同。但是它不仅可以操作字符,还可以操作其他媒体文件。
字节流一般不用flush,除非用到了缓冲区。
不要用字符流操作媒体文件,无法被解析。因为查表查不到。
输出流OutputStream
1 | public static void demo_write() throws IOException { |
输入流InputStream
1 | public static void demo_read() throws IOException { |
键盘录入
Windows中回车是两个字符,’\r’对应数字13,’\n’对应数字10。因此如果读取回车符号会得到13与10。
默认的输入与输出设备均不用关流,如果关了则不能再次获取,除非重启设备。
将系统输入与字节流进行关流,然后调用字节流的方法即可。
1 | InputStream in = System.in; |
集合的清空为clear,StringBuilder的为delete
获取键盘数据
获取键盘录入数据,转为大写,利用over结束
1 | public static void readKey2() throws IOException { |
会发现使用到的判断跟BufferedReader中的readLine方法很接近。因此想利用readLine来做。
转换流
InputStreamReader :字节到字符的桥梁。解码。
OutputStreamWriter:字符到字节的桥梁。编码。
BufferedReader中有readline方法,如果要使用到BufferedRead方法,需要传入的是字符流对象,但是如果想让字节流也是用其方法,需要将字节流转变为字符流。
可以使用InputSreamReader。需要考虑编码的问题。在字符流对象中。将字节流变成字符流
从名字判断。后缀为父类,前缀为功能。
因此可以将上面的代码改写,直接获取一行字符串即可。
1 | //字节流 |
字符与字节的区别
读取汉字的时候,一个汉字字符为2个字节,如果用字节读就会读2次,然后将2个字节对应成数字单独输出;如果是字符流,那么会一下子读2个,只读1次,然后查表找对应的字符。
字节->字符,解码。因为从看不懂的变成看得懂的。使用IuputStreamReader
字符->字节,编码。因为从看的懂的变成看不懂的。使用OutputStreamWriter
示意图:从键盘输入到控制台输出
字节流从键盘拿数据,变成字符流使用字符流装饰类来提高效率,因此数据就读取到了缓冲区中,然后从缓冲区中写数据到字符输出流,数据到了字符输出流,目的地是out.控制台,需要将字符流转换成字节流,然后进行输出字节数据。
键盘录入到控制台输出
1 | 键盘录入到控制台输出 |
重点代码
1 | //键盘录入 |
流操作规律
之所以要弄清楚这个规律,是因为流对象太多,开发时不知道用哪个对象合适。
想要知道开发时用到哪些对象,只要通过四个明确即可。
1, 明确源和目的(汇)
源: InputStream Reader
目的:OutputStream Writer
2, 明确数据是否是纯文本数据
源:是纯文本:Reader
否:InputStream
目的:是纯文本:Writer
否:OutputStream
到这里就可以明确需求中具体要用哪个体系。
3, 明确具体的设备。
源设备:
硬盘:File
键盘:System.in
内存:数组
网络:Socket流
目的设备:
硬盘:File 控制台:System.out
内存:数组
网络:Socket流
4, 是否需要其他额外功能。
1, 是否需要高效(缓冲区)?
是,就加上buffer。
2, 是否需要转换
是
源:InputStreamReader 字节流->字符流
目的:OutputStreamWriter 字符流->字节流需求1:复制一个文本文件。
1,明确源和目的。
源:InputStream Reader
目的:OutputStream Writer
2,是否是纯文本?
是!
源:Reader 目的:Writer
3,明确具体设备
源:
硬盘:File
目的:
硬盘:File
FileReader fr = new FileReader(“a.txt”);
FileWriter fw = new FileWriter("b.txt");4,需要额外功能吗?
需要,需要高效。
BufferedReader bufr = new BufferedReader(new FileReader(“a.txt”));
BufferedWriter bufw = new BufferedWriter(new FileWriter(“b.txt”));
需求2:读取键盘读入信息,并写入到一个文件中。
1,明确源和目的。
源:InputStream Reader
目的:OutputStream Writer
2,是否是纯文本呢?
是 源:Reader
目的 :Writer
3,明确设备
源: 键盘:System.in
目的
硬盘:File
InputStream in = System.in;
FileWriter fw = new FileWriter(“b.txt”);
这样做可以完成,但是麻烦。将读取的字节数据转成字符串,再由字符流操作。
4,需要额外功能吗?
需要,转换。将字节流转换成字符流。因为明确的源是Reader,这样操作文本数据最便捷。
所以要将已有的字节流转成字符流。使用字节–>字符。InputStreamReader
InputStreamReader isr = new InputStreamReader(System.in);
FileWriter fw = new FileWriter(“b.txt”);
还需要功能吗?
需要:想高效
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bufw = new BufferedWriter(new FileWriter(“b.txt”));
需求3:将一个文本文件数据显示在控制台上
1,明确源和目的。
源:InputStream Reader
目的:OutputStream Writer
2,是否是纯文本呢?
是
源:Reader
目的 :Writer
3,明确具体设备
源: 硬盘:File
目的:
控制台:System.out
FileReader fr = new FileReader(“a.txt”);
OutputStream out = System.out;//对象类型是PrintStream
4,需要额外功能吗?
需要,转换。
FileReader fr = new FileReader(“a.txt”);
OutputStreamWriter osw = new OutputStreamWriter(System.out);
需要,高效。
BufferedReader bufr = new BufferedReader(new FileReader(“a.txt”));
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));
需求4:读取键盘录入数据,显示在控制台上
1,明确源和目的。
源:InputStream Reader
目的:OutputStream Writer 2,是否是纯文本呢?
是
源:Reader
目的 :Writer
3,明确设备
源: 键盘:System.in
目的: 控制台:System.out
InputStream in = System.in;
OutputStream out = System.out;
4,明确额外功能?
需要转换,因为都是字节流,但是操作的却是文本数据。
所以使用字符流操作起来更方便。
InputStreamReader isr = new InputStreamReader(System.in);
OutputStreamWriter osw = new OutputStreamWriter(System.out);
为了将其高效操作。
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));
转换流的编码解码
GBK中一个中文对应2个字节,在UTF-8编码中,一个中文对应3个字节。
需求5:将一个中文字符串数据按照指定的编码表写入到一个文本文件中
1,目的。OutputStream Writer
2,是纯文本,Writer
3,设备:硬盘File
FileWriter fw = new FileWriter(“a.txt”);
fw.write(“你好”);
注意:既然需求中已经明确了指定编码表的动作。
那就不可以使用FileWriter,因为FileWriter内部是使用默认的本地码表。
只能使用其父类,OutputStreamWriter。
OutputStreamWriter接收一个字节输出流对象,既然是操作文件,那么对象应该是FileOutputStream
OutputStreamWriter = osw = new OutputStreamWriter(new FileOutputStream(“a.txt”),charsetName);
需要高效不?
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(“a.txt”),charsetName));
什么时候使用转换流呢?
1,源或者目的对应的设备是字节流,但是操作的却是文本数据,可以使用转换流作为桥梁,提高对文本操作的便捷。
2,一旦操作文本涉及到具体的指定编码表,必须使用转换流。
1 | //字节流加码表变成字符流。 |
打印流
当想把数据保持原样,使用打印流最方便。按照需求使用,如果要保证文件的大小,使用之前的输出流,如果要保持数据原样,使用打印流。打印流只负责目的。
PrintWriter与PrintStream
PrintStream
System.out类型就为PrintStream类型。PrintStream为其他输出流添加了功能,使他们能够方便的打印各种数据值表现形式。不抛出IO异常。在需要写入字符而不是写入字节的情况下,应该使用PrintWriter。
1,提供了打印的放法,可以对多种数据类型值打印。并保持数据的表示形式。
2,它不抛IO异常。
构造函数,接收三种类型的值
1, 字符串路径
2, File对象
3, 字节输出流
write只写最低8位,print将值变为字符串再打印,保持原样。
1 | //输入int为32位,只保留后8位,将前24位截取,这样在记事本中表现为a |
PrintWriter
之后用的很多,服务端所用的就为PrintWriter。
构造函数参数:
1, 字符串路径
2, File对象
3, 字节输出流
4, 字符输出流
直接用println方法写入数据
public PrintWriter(OutputStream out,boolean autoFlush)
public PrintWriter(Writer out, boolean autoFlush)
如果传入true,println,printf,format方法将刷新缓冲区。
1 | //读键盘 |
序列流
SequenceInputStream
序列流只负责源。
将多个输入流合并为一个流。
将最后一个流的-1作为整个流的-1。
SequenceInputStream(Enumerationextends InputStream> e)
初始化新创建 SequenceInputStream通过记住参数,它必须是一个 Enumeration产生对象,它们的运行时类型是 InputStream 。
构造器有两种,一种是传入参数为两个字节输入流,一个是字节输入流的枚举。而Vector中有枚举,直接使用Vecor效率太低于是使用ArrayList。但是只能获取迭代器,这样便想到去new一个迭代器,需要去实现其方法,因为与迭代器只有名称不一样,因此可以利用获取的迭代器方法来实现枚举。如果用匿名内部类来实现,而内部类使用临时变量,此临时变量需要被final修饰。但是这样比较麻烦,于是想到去Collections集合工具类中找方法,此方法返回枚举,接收集合对象,可以使用集合对象的方法,于是可以使用
Enumeration
这样可以有一种思路,即相同功能对象可以利用另一个的方法来实现自身,而不用自己去实现。
文件切割
要切几个文件用几个输出流。
如果要将文件存入指定路径,可以将路径与文件后缀名封装成文件对象,因为文件对象的构造器中可以加入文件对象。
文件合并
如果要将多个文件写入同一个文件或者将文件碎片合并到一个文件,要用到序列流。序列流需要用到枚举,而枚举可以通过集合获取到。先将字节输入流存入ArrayList集合,然后用Collections工具类获取集合的枚举。这样从序列流中将数据写入到指定的路径下。如果要指定路径+文件后缀名,可以new File(dir,”1.mp3”);
序列化
操作对象(装饰类)
ObjectInputStream与ObjectOutputStream,操作对象的流。
把对象生命周期延迟,存储在硬盘上实现持久化。把对象从堆内存存储在了硬盘上。
ObjectOutputStream
为了实现额外功能,相当于装饰类,将字节流对象关联。
序列化的条件为:
- 要存储的对象必须实现Serializable接口
- 该类的所有属性要是可序列化的。如果有一个属性是不需要可序列化的,该属性需要注明是瞬态的,使用
transient关键字说明
1 | public static void writeObj() throws IOException { |
ObjectInputStream
ObjectInputStream对以前使用ObjectOutputStream写入的基本数据和对象进行反序列化。只能读ObjectOutputStream写入的数据。
1 | //对象读取流,因为是增强功能因此要传入字节输入流。 |
因为一次只能读一个对象,因此如果想多读几个对象,可使用以下方法。
1 | try { |
Serializable
如果用原来的class文件,用更改后的类来接收,会报错,异常为InvalidClassException。
类实现Serializable接口,Serializable为标记接口,序列化运行时使用称为serialVersionUID的版本号与每个可序列化类相关联。该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载类的ID号与发送者的类的版本号不同,反序列化会导致InvalidClassException。
因此Serializable用于给被序列化的类加入ID号。用于判断类和对象是否是同一个版本。如果可序列化类未显示声明serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认serialVersionUID。强烈建议所有可序列化都显示声明serialVersionUID值,因为计算默认ID对类的详细信息有较高的敏感性,根据编译期的不同可能千差万别。
一个可序列化的类可以通过声明一个名为”serialVersionUID”的字段来显式地声明它自己的serialVersionUID,该字段必须是static,final和long类型
private static final long serialVersionUID = 9527L;
只要ID号一样,就算类文件变化了,那就依然可以读出,在服务器上可能会用到。
无法被写入的属性(static transient)
静态修饰
如果类中某一属性被静态修饰,则堆内存中没有此属性,因此无法被写入到对象中。对象输出流只能写入非静态的属性和非瞬态的。
原因:
- 静态优先与非静态加载到内存中,被static修饰的成员变量不能被序列化,序列化的都是对象
transient关键字
短暂的,暂时的。如果某一属性不是公用的,不能被static修饰,但是又不想写到对象中,可以使用transient关键字。
非静态数据不想被序列化可以使用这个关键字修饰。
序列化集合
若要将对象存储在集合中,再进行序列化,需要将可序列化的对象存入集合,然后用序列化流的写Object方法写入,再用反序列化流的读Object方法读,要注意的是,方法为writeObject()和readObject()!!!
1 | public static void main(String[] args) throws IOException, ClassNotFoundException { |
File类
IO流只能操作文件中的数据。
而File类用来将文件或者文件夹封装成对象,方便对文件与文件夹的属性信息进行操作。
File对象可以作为参数传递给流的构造函数。
构造函数
1 | //可以将一个已存在的,或者不存在的文件或目录封装成File对象。 |
不同的操作系统分隔符不一样。之前在系统类中使用字段file.separator获取分隔符。现在使用File.separator来获取分隔符。
1 | //为了系统兼容,使用System.getProperty("file.separator") |
常用方法
获取修改时间
1 | //作用:判断修改时间,加载修改后的新文件 |
File对象的常用方法
1,获取
1.1获取文件名称
String getName();
1.2获取文件路径
String getPath();
String getAbsolutePath();
1.3获取文件大小
long length();
1.4获取文件修改时间
long lastModified()得到毫秒值
2,创建与删除
如果文件或目录存在就不会创建。
delete慎用,删除成功无法从回收站中找回。
delete无法删除原因:
文件夹中有目录;正在被流使用
2.1 文件创建
boolean createNewFile();
和输出流不一样,如果文件不存在,则创建;如果文件存在,则不创建。
2.2 文件删除
boolean delete();
2.3 文件夹创建
boolean mkdir();//make directory
boolean mkdirs();//创建多级目录文件夹
2.4 文件夹删除
boolean delete();//如果文件夹中有目录无法删除
如果是多级目录下的文件,则只删掉最子集的目录,前面的父目录无法删掉。
3,判断(很实用)
在判断是否为文件或者是否是目录之前,要判断其是否存在。
boolean exists();//是否存在
boolean isFile();//是否是文件
boolean isDirectory();//是否是目录
4,重命名
需要有2个File,第二个File为命名后的文件位置
Boolean renameTo(File file);
相当于剪切操作,可以从一个盘符移动到另一个。先关联两个文件,然后用文件去调用方法
File f1 = new File(“d:\1.jpg”);
File f3 = new File(“d:\1\2.jpg”);
boolean b2 = f1.renameTo(f3);
System.out.println(“文件剪切是否成功”+b2);
5,系统根目录和容量获取
static Files[] listRoots();//获取系统中所有盘符,静态方法
long getFreeSpace();//获取可用容量
long getTotalSpace();//获取总容量
long getUsableSpace();//获取虚拟机可用容量
6,获取目录内容及文件名过滤器
实现FilenameFilter接口。使用list方法。
使用list只能获取当前目录下的名称。
String[] list();
String[] list(FilenameFilter filter)
获取当前目录下的文件以及文件夹的名称,包含隐藏文件
调用list方法的File对象中封装的必须是目录。否则会发生空指针异常
如果访问的是系统级目录,也会发生空指针异常
如果目录存在,但是没有内容,会返回一个数组,但是长度为0
文件名过滤器:过滤文件名
调用list(FilenameFilter),调用list方法时候需要传入指定的过滤器对象,过滤器需要实现accept方法。
1 | File file = new File("d:\\"); |
过滤器原理,先调用list方法获取指定路径下的全部目录,将 其存进数组,遍历数组,只要符合FilenameFilter.accept(dir,name)条件的,就进行存储,如accept方法可使用name.endsWith(指定文件格式)。
如果需要对过滤器传入参数,需要对过滤器添加构造函数,过滤器持有一个私有的字符串变量,通过构造函数来获取字符串变量,然后accept方法进行判断。
7,获取目录下文件对象及过滤
实现FileFilter接口。使用listFiles方法
File[] listFiles()
返回一个抽象路径名数组,表示由该抽象路径名表示的目录中的文件。
File[] listFiles(FileFilter filter)
返回一个抽象路径名数组,表示由此抽象路径名表示的满足指定过滤器的目录中的文件和目录。
File[] listFiles(FilenameFilter filter)
返回一个抽象路径名数组,表示由此抽象路径名表示的满足指定过滤器的目录中的文件和目录。
文件类型过滤器
如可以过滤非隐藏文件
深度遍历文件夹
思路:
1、 将待深度遍历的目录关联File对象
2、 使用自定义方法来获取其所有子目录及文件
3、 方法怎么弄呢?首先要获取该目录下所有文件数组,遍历此数组,如果是目录则使用自定义功能继续遍历,如果不是目录则将文件名输出。这样便实现了遍历输出。
4、 但是这样不美观,因此需要记录下每个目录的层级信息。不能直接在方法中定义一个数,因为每次调用方法都会将这个数重置,那么需要在递归时传递给下一次递归。则进入下一次递归后将层级++,便实现了层级信息的记录。
5、 那么要如何美观输出呢?每多一个层级,就多一点空格,因为不知有多少字符串,所以使用StringBuilder来实现可变字符串,依靠层级数信息来添加空格信息。
1 | public class FileTest { |
递归
递归:函数自身直接或间接调用自身。
一个功能在被重复使用并每次使用时参与运算的结果和上一次调用有关。这时可以用递归来解决问题。
注意:
1,递归一定要明确条件,否则容易栈溢出。即最小收敛情况。
2,注意一下递归的次数。太多次容易栈溢出。超过栈内存就会报错。
递归中单独定义变量,每次调用方法,各个方法都会持有该变量的引用,如果想让此变量被下一次递归使用,则要作为参数传递给下一次。
二进制转换
调用时候最先出栈的先输出,因此先输出1%2,然后3%2,最后6%2,因此结果是110.如果toBin()在输出下面,那就是011,因为先输出6%2,3%2,1%2。
1 | public static void toBin(int num){ |
求和
1 | public static int getSum(int num){ |
递归图解:运行sum(5),sum(4),sum(3),sum(2),sum(1),然后sum(1)出栈,返回1,sum(2)出栈,返回2+1,sum(3)出栈,返回3+3,sum(4)出栈,返回4+6,sum(5)出栈,返回5+10。
Properties集合
Map
|--Hashtable |–Properties:
Properties集合:
特点
1,该集合中的键和值都是字符串类型。
2,集合中的数据可以保存在流中(store),或者从流中获取(load)。
通常该集合用于操作以键值对形式存在的配置文件。
常用方法
1,存储
单个存储
Object setProperty(String key, String value);
也可以用来覆盖,键相等,值覆盖
使用的是Hashtable的put方法
2,获取
单个获取
String getProperty(String key);
全部获取(转为Set集合)
Set
返回此属性列表中的一组键,其中键及其对应的值为字符串,包括默认属性列表中的不同键,如果尚未从主属性列表中找到相同名称的键。
3,与流对象关联
public void list(PrintStream out);将此属性列表打印到指定的输出流。 此方法对调试非常有用。
4,持久化存储
可以关联字节流或字符流,comment为相关注释,不要写中文。
void store(OutputStream out, String comments);
void store(Writer writer, String comments)
5,获取输入流
void load(InputStream inStream)
从输入字节流读取属性列表(键和元素对)。
void load(Reader reader)
以简单的线性格式从输入字符流读取属性列表(关键字和元素对)。
1 | /* |
获取系统信息
1 | //获取系统的键值信息 |
模拟load方法
1 | /* |
从文件中获取配置信息
1 | Properties prop = new Properties(); |
配置信息
简单配置用Properties,复杂配置使用XML。相当于标签。
简单配置
复杂配置
XML使用起来更为清晰。
IO流其他类
RandomAccessFile(多线程写入)
随机访问文件。看到这个类的名字,纠结。父类为Object。不是IO体系中的子类。此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型byte数组。
利用seek方法修改指针位置,即可实现在指定位置的随机读写。一般要求数据有规律。
应用:利用多线程,对同一个文件分段同时写入数据。多线程写入。
特点:
1, 该对象既能读,又能写。
2, 该对象内部维护了一个大型byte数组,并通过指针可以操作数组中的元素。
3, 可以通过getFilePointer方法获取指针的位置,和通过seek方法设置指针的位置。
4, 其实该对象就是字节输入流和输出流进行了封装。
5, 该对象的源或者目的只能是文件。通过构造函数就可以看出。
6,如果文件不存在,则创建;如果文件存在,不创建。
构造方法
RandomAccessFile(File file, String mode) 创建一个随机访问文件流,从File参数指定的文件读取,并可选地写入。
RandomAccessFile(String name, String mode) 创建随机访问文件流,以从中指定名称的文件读取,并可选择写入文件。
mode的含义
局限性:目的只能是文件,不能是其他输出流。
write(byte b),如果传入String类型,需要用getBytes()方法转成字节。
按字节写入,如果传入一个比较大的int类型数,会被截断。
读写方法
void write(byte[] b) 从指定的字节数组写入 b.length个字节到该文件,从当前文件指针开始。
void writeInt(int v) 将 int写入文件为四个字节,高字节为首。
int read(byte[] b) 从该文件读取最多 b.length个字节的数据到一个字节数组。
int readInt() 从该文件读取一个带符号的32位整数。
1 | //使用RandomAccessFile对象写入一些人员信息,比如姓名和年龄。 |
随机读写
通过seek方法可以随时设置指针的位置,可以实现随机的读取,只要指定指针的位置即可。
因为数据都在byte数组里面存着,因此如果某位置有数据,因此如果再从0写入,会将原来的数据进行覆盖。因此可以修改数据。
再设置seek方法,将数据写在想要的位置即可。
管道流
PipedInputStream和PipedOutputStream。
将两个流关流,只读指定流的数据。
输入输出可以直接进行连接,通过结合多线程使用。
管道输入流应连接到管道输出流; 管道输入流然后提供写入管道输出流的任何数据字节。通常,一个线程从PipedInputStream对象读取数据,并且其他线程将数据写入相应的PipedOutputStream 。 不建议尝试从单个线程使用这两个对象,因为它可能会使线程死锁。 管道输入流包含一个缓冲区,在读取操作中将读取操作与限制内的操作相分离。 如果向连接的管道输出流提供数据字节的线程不再存在, 则称管道为broken 。
1 | public class PipedStream { |
操作基本数据类型(装饰类)
DataInputStream与DataOutputStream。
用于操作基本数据类型。如果使用修改版UTF-8写入,则只有此流对应的方法才为你那个读入。
数据输入流允许应用程序以独立于机器的方式从基础输入流读取原始Java数据类型。 应用程序使用数据输出流来写入稍后可以被数据输入流读取的数据。
普通的write会将整数只保留后8位。
为装饰类,需要传入相应的字节输入流与字节输出流。
1 | public static void writeDate() throws IOException { |
操作字节数组(源、目的为内存)
一般操作的数据不大。
ByteArrayInputStream与ByteArrayOutputStream
源和目的都是内存。没有调用底层资源、
ByteArrayOutputStream该类实现了将数据写入字节数组的输出流。 当数据写入缓冲区时,缓冲区会自动增长。 可以使用toByteArray()和toString()检索数据。
关闭ByteArrayOutputStream没有任何效果。 在关闭流之后,仍可以调用此类中的方法,而不生成IOException 。
A ByteArrayInputStream包含一个内部缓冲区,其中包含可以从流中读取的字节。 内部计数器跟踪由read方法提供的下一个字节。
关闭一个ByteArrayInputStream没有任何效果。 该流中的方法可以在流关闭后调用,而不生成IOException 。
输入流源就是数组。
操作字符数组
CharArrayReader与CharArrayWriter
源为字符数组。
操作字符串
StringReader与StringWriter
源为字符串。
编码表
编码表:将各个国家的文字用数字表示,并一一对应,形成一张表。
常见的编码表
ASCII:美国标准信息交换码。
用一个字节的7位可以表示
ISO8859-1:拉丁码表。欧洲码表
用一个字节的8位表示,兼容ASCII码表。
GB2312:中国的中文编码表。
GBK:中国的中文编码表升级,融合了更多的中文文字符号。
Unicode:国际标准码,融合了多种文字。
所有文字都用两个字节表示,Java语言使用的就是unicode
UTF-8:最多用三个字节来表示一个字符。Unicode to其他,如果可以用一个字节装下(ASCII码)用一个,两个装的下用两个,三个装的下用三个。
编码解码
简单编码解码
利用getBytes(编码名)和new String(byte[] b,编码名)来分别实现编码和解码。
1 | /* |
编码解码问题
如果编码编错了,解不出来。如果编对了,解错了,有可能有救。
当你好用GBK编码,获得4个数,用iso8859-1解码,获取4个未知字符,再利用iso8859-1对四个未知字符进行编码,获取4位数字,然后利用GBK进行解码,获得你好。
1 | //字符串编码按照本地的编码来 |
一种应用
当数据从本地提交到服务器,在服务器先解码然后以流的形式传回,但是不是中文编码,因此先用服务器自己的编码表再编码一次,然后用GBK解码,就可以得到想要的信息。
如果使用utf-8先解码再编码,很有可能会解码失败,因为使用GBK获取的数字在utf-8中找不到对应的码,因此会变成未知字符,此时码已经发生了变化,因此就无法被还原。而使用iso8859-1可以还原是因为里面没有用到中文而且都是单字节编码。
UTF-8编码
一个字节时,打头位固定为1,2个字节时,前几位也为固定位。
当字节流读数据,读到第一个字节开头为110,然后读第二个字节开头为10,马上去查表。然后读到0,读一个字节就去查表。然后毒药1110,再读10,再读10,然后去查表。
一个字节,打头为0;两个字节,打头为2个1,三个字节,打头为3个1。
联通问题
联通在记事本中打开为乱码。解码出了问题。因为联通写入记事本,默认的是使用UTF-8存入,解码的时候也使用UTF-8。
1 | String str = "联通"; |
网络
网络模型概述
OSI(Open System Interconnection开放系统互联)参考模型
TCP/IP模型
七层模型
1层物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是1、0转化为电流强弱来进行传输,到大目的后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
2层数据链路层:主要将从物理层接收的数据进行MAC地址(网卡的地址,每个网卡唯一)的封装与解封装。常把这一层的数据叫做帧。这一层工作的设备是交换机(实现互联),数据通过交换机来传输。
3层网络层:主要将从下层接收到的数据进行IP地址(例192.168.0.1)的封装与解封装。看数据到底发向哪一台主机。在这一层工作的设备是路由器(数据包方向的定义),常把这一层的数据叫做数据包。
4层传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。
5会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接收会话请求(设备之间需要互相认识,可以使IP也可以是MAC或者是主机名)
6表示层:主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等(也就是把计算机能够识别的东西转成人能识别的东西(如图片、声音等))。
7应用层:主要是一些终端的应用(应用软件),比如FTP(各种文件下载),WEB(IE浏览),QQ之类的(可以把它理解成我们在电脑屏幕上可以看到的东西,就是终端应用)。
对数据加上每一层的标识,便于解析。逐层包装标识(封包),逐层解析(拆包)。
七层模型过于繁琐,因此有了TCP/IP参考模型。
1主机至网络层:物理层+数据链路层。(交换机为一部分)
2网际层:网络层
3传输层:原传输层(重要)
5 应用层:会话层+表示层+应用层。(Java Web开发)
网络通讯要素
IP地址
端口号
传输协议
IP地址
IPV4:有4段,一段的最大值为一个Byte,即2^8,0-255。最大值为255。
IPV6:融入了字母。因为地址够用,因此很多设备都可以拥有独立的IP地址,物联网的时代。
IP地址:InetAddress
网络中设备的表示
不易记忆,可用主机名
本地回环地址:127.0.0.1 主机名:localhost 用于在没有互联网时本机访问本机。如果ping本机地址有问题,网卡可能出问题了。测试网卡。
InetAddress
存在于网际层。
可以通过主机名称或者主机地址字符串获得ip对象。
1 | //获取本地主机IP地址对象 |
IP地址太多记不住,给主机命名,记住名字即可。
com一般属于商业化组织,营利性;org属于非营利性。
cn标识所属国家类别。
IP地址与名字有对应关系,互联网上公共的服务器中存放着IP地址和名称的对应关系。叫做域名解析DNS,机器叫域名解析服务器(记住了IP地址)。不指定DNS解析地址,宽带服务商解析,信息发给他,他再发给DNS服务器。
现在一个IP地址可能分配给多台主机使用。
如果想提高解析速度,可以在本机中创建域名解析列表。即host文件,本地域名解析列表。域名解析最先走的为本地解析列表,如果解析失败再走互联网的解析。自己可以创建对应名称来访问本机,其他人使用此host文件中的域名或地址无法访问到本主机,因为其机器上没有此列表。如果想在局域网中所有人都可以使用此解析列表,可以在某台主机上使用DNS解析软件,然后其他主机的DNS解析地址指向此主机即可。
如果想屏蔽某些网站,可以将其域名与本地ip地址127.0.0.1关联,这样可以屏蔽公共解析,指向本地解析。
IP地址:192.168.1.1
子网掩码:255.255.255.0
子网掩码前3个均为255,代码IP地址前三位均为网络位,最后一位为IP地址位。从0-255,0代表网络位,不可用,因此1-254可用,255不属于IP地址而是广播地址。如果发到192.168.1.255,代表把消息发到192.168.1.0网络上所有存活的机器上。
端口
逻辑端口,给应用程序分配数字标识。
用于标识进程的逻辑地址,不同进程的标识。
有效端口:065535(2^16),其中01024系统使用或保留端口。
禁用端口:可以防止某些应用程序禁用互联网。
传输协议
通讯规则
常见协议:TCP,UDP
UDP
数据报文协议
将数据及源和目的封装成数据包中,不需要建立连接
每个数据报的大小限制在64k内
因无连接,是不可靠协议
不需要建立连接,速度快
如QQ聊天,视频通讯。
TCP
传输控制协议。
建立连接,形成传输数据的通道
在连接中进行大数据量传输
通过三次握手完成连接,是可靠协议
必须建立连接,效率会稍低
下载数据
Socket
插座,套接字
socket就是为网络服务提供的一种机制
通信的两端都有Socket
网络通信其实就是Socket间的通信
数据在两个Socket间通过IO传输。
UDP传输
DatagramSocket与DatagramPacket
建立发送端,接收端
建立数据包
调用Socket的发送接收方法
关闭Socket
发送端与接收端是两个独立的运行程序
DtagramSocket:表示用于发送和接收数据报包的套接字。
可以直接new对象,有发送和接收数据包的方法。
DatagramPacket:表示数据报包数据报包实现无连接包投递服务。不对包投递做出保障。
构造时有的是发送的,有的是接收的。发送的数据包有目的地址,接收不需要。带有IP对象的均为用来发送的。发送多接收少。
UDP发送端与接收端哪个先连接都可以。
发送端指定的端口为接收端的端口,而接收端获取的端口为发送端的端口。
发送端
1 | /* |
接收端
1 | /* |
TCP传输
Socket和ServerSocket
建立客户端和服务器端
建立连接后,通过Socket中的IO流进行数据的传输
关闭Socket
同样,客户端与服务器端是两个独立的应用程序。
Socket类实现客户端套接字。ServerSocket类实现服务器套接字。
客户端向服务端发送信息建立通道,通道建立后服务器端向客户端发送信息。
客户端一般初始化时要指定对方的IP地址和端口,IP地址可以是IP对象,也可以是IP对象字符串表现形式。
建立通道后,信息传输通过Socket流,为底层建立好的,又有输入和输出,想要获取输入或输出流对象,找Socket来获取。为字节流。getInputStream()和getOutputStream()方法来获取输入流和输出流。
服务端获取到客户端Socket对象,通过其对象与Cilent进行通讯。
客户端的输出对应服务端的输入,服务端的输出对应客户端的输入。
TCP必须先开服务端。
如果名称为out,in的视为Socket流,如果不是就是一般流。
tcp使用的时候可能会出现两端都在等待的情况,原因可能是数据没有发送出去。最大原因在于有阻塞式方法。
客户端
1 | //客户端发数据到服务端 |
服务端
1 | //服务端接收客户端发送过来的数据,并打印在控制台上。 |
服务端与客户端交互
服务器接收到数据后,利用客户端的Socket对象的输出流写入数据,客户端利用Socket流的输入流进行数据接收。
客户端
1 | //读取服务端返回的数据,使用Socket读取流。 |
服务端
1 | //使用客户端Socket对象的输出流给客户端返回数据 |
服务器端原理,并发访问,只有一个端口对外提供,当进来一个客户端Socket,阻塞,新建线程去执行,线程任务就是读写操作。然后再进来一个客户端,再利用新线程去执行,为了避免线程过多,一个线程结束后就结束线程。把连接进来的客户端封装在线程中。
常见客户端与服务端
最常见客户端:
浏览器:IE。
最常见的服务器:
服务器:Tomcat。
http:应用层协议,超文本传输控制协议。文字带颜色,大小,图片带声音。使用语言为html。
定义了web浏览器与服务器的通信规则。浏览器中有解析http协议的解析引擎。
FTP:文件传输协议
Web服务器默认端口:80
Tomcat服务器对外提供接口。interface Servlet。必须直接或间接实现。Tomcat对外提供Web资源访问。
访问服务器:http://主机名:8080/myweb
会自动去webapps下去寻找。
服务器必须对外提供可访问资源,即Web应用程序。webapps下存放的为web资源。
客户端和服务端原理
了解原理:
1, 自定义服务端,使用已有的客户端IE,了解一下客户端给服务器端发了什么请求。
发送的请求是
GET / HTTP/1.1 请求行 请求方式 /myweb/1,html 请求的资源路径 http协议版本(1.0或者1.1,1.1更常用)请求方式包括GET,POST
请求消息头 属性名:属性值(键值对)
Accept:
服务器可以支持的app
text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
支持的语言
Accept-Language: zh-Hans-CN,zh-Hans;q=0.8,en-US;q=0.5,en;q=0.3
Upgrade-Insecure-Requests: 1
用户信息,系统版本
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763
支持的压缩方式,服务器发送信息时将信息压缩,客户端解压缩
Accept-Encoding: gzip, deflate
访问的主机
Host: 127.0.0.1:9090
Connection: Keep-Alive
请求头和请求体之间有空行
请求体:可能会有注册信息等。
2, 自定义浏览器
服务端发回应答消息
HTTP/1.1 403 Forbidden 应答行,http的协议版本 应答状态码 应答状态描述信息
200代表成功,描述信息OK 404 not found 找不到页面
应答消息属性信息。属性名:属性值
Server: bfe
Date: Tue, 05 Mar 2019 15:26:23 GMT
最后修改时间,带着信息访问,如果修改日期一致本地缓存界面与服务器端一样,发新的状态码。
Last-Modified:一堆日期
发送字节数
Content-Length: 0
收到数据类型
Content-Type: text/plain; charset=utf-8
Connection: close
//有一行空行
应答体
处理请求并给予应答。
可以网页输入http://主机名:指定端口号,即可以访问自定义的服务端。
URL
协议解析对象
浏览器向服务器发送http请求信息,然后浏览器发送应答消息头和应答体。浏览器中只显示应答体,而应答消息头被浏览器解析引擎解析。
可以使用URL对象对应答消息头进行解析。URL底层挂的是URLConnection
类URL代表一个统一资源定位符,是指向互联网“资源”的指针。资源可以是简单的文件或目录,也可以是对更为复杂的对象的引用,例如对数据库或搜索引擎的查询。
URI统一资源标识符引用。
每个URL都是URI,但不一定每个URI都是URL。URI还包括个子类URN,统一资源名称。mailto、news和isbnURI都是URN的示例。
URL对象可以直接解析URL地址。getProtocol(),获取协议。
1 | //获取信息 |
InputStream openStream()打开此URL的连接并返回一个用于从该连接读入的InputStream。
1 | //只输出应答体,不输出应答消息头 |
其中openStream()底层使用的是openConnection().getInputStream()
URLConnection
URL连接器
1 | //获取url对象的URL连接器,将连接封装成了对象:java中内置的可以解析的具体协议的对象+socket |
conn打印结果如下
前面http地址为http底层实现。
String getHeaderField(String ) 获取相关属性,
如使用String value = conn. getHeaderField(“Content-Type”),获取内容类型,然后用相关的解析器去解析。
可以获取输入和输出流,因此相当于使用了Socket,但是加入了协议。
URL中openStream原理为先获取连接,然后获取流
1 | //获取url对象的URL连接器,将连接封装成了对象 |
因此以后浏览器就不用使用Socket,而是使用
1 | String str_url = new String("http://192.168.1.100:8080/myweb/1.html?name=lisi"); |
网络结构
1,C/S Client/Server
特点:
该结构的软件,客户端和服务端都需要编写。
开发成本较高,维护较为麻烦。
可能会出现版本差异
好处:
客户端在本地可以分担一部分运算。
网络游戏就是客户端,因为很多数据存储在本地,位置坐标实时与主机交换。
2,B/S Browser/Server
特点:
该结构的软件,只开发服务器端,不开发客户端,因为客户端直接由浏览器取代。
开发成本相对低,维护更为简单。
缺点:
所有运算都在服务器端完成。
反射机制
概述与应用场景
反射中涉及到的对象均在java.lang.reflect包中。Constructoe,Filed,Method。
AccessibleObject类是Field、Method、和Constructor对象的基类。它提供了将反射的对象标记为在使用时取消Java语言访问控制检查的能力。
java反射机制是在运行状态中,对于任意一个类(class文件),都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性。
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
动态获取类中信息,就是java的反射机制。
可以理解为对类的解剖。
软件的功能扩展利用对外提供接口实现。
一个类实现了软件对外暴露的接口,会在程序的配置文件中写入,程序读取此配置文件,会去寻找接口实现类的class文件。如果找到则加载此文件,并获取该文件里所有内容。拿到字节码文件就可以新建对象,因为其中有构造函数。
如果想要对指定名称的字节码文件加载并获取其中的内容并调用。
实现方法:这时使用到了反射技术。
优点:极大的提高了程序的扩展性。
Tomcat提供的接口为Servlet,服务器端脚本程序片段。将实现接口的类名称写入配置文件,软件便可以动态加载此类中的内容。利用的是反射技术。
需要接口+配置文件。
学习框架:框架作用,配置文件怎么用,常用对象的用法。
反射技术提高了扩展技术,用起来简单(用户只面对配置文件)。
细节和Class对象
传参通过配合文件完成。
拿到类以后,获取指定类中的信息。
反射的过程:由Class类来完成
Class类用来表述二进制字节码文件。可以new对象,提供获取到字节码文件中的内容,如名称、字段、构造函数、一般函数。该类就可以获得字节码文件中的所有内容。反射就是依靠该类来完成的。
想要对一个类文件进行解剖,只要获取到该类的字节码文件对象即可。
获取Class对象的三种方式
要想对字节码文件进行解剖,必须要有字节码文件对象。如何获取其对象?
1 | /* |
获取Class中的构造函数
获得空参的对象
1 | //早期:new的时候,先根据被new的类的名称找寻该类的字节码文件,并加载进内存, |
如果想要获得要输入参数的对象,则要获取构造函数
1 | // cn.zc.bean.Person p = new cn.zc.bean.Person("小强", 39); |
获取Class中的字段
如果字段是私有的,则不能直接访问,需要先取消权限检查,称为暴力访问,不建议使用。
1 | /* |
获取Class中的方法
1 | /* |
反射演示
想要不修改主函数,只通过修改配置文件来让不同的设备运行。程序要对外暴露一个接口PCI,要实现开和关的方法,设备要实现此接口。
关键:对外暴露接口,加载配置文件,新功能实现此接口,获取到Class对应的对象,将此接口对象传入相应的方法,其他的就很简单了。
思路:
1、 将配置文件关联到文件对象,新建Properties对象,新建流将文件中数据load进Properties中。
2、 遍历键值对,逐个获取到类名。根据类名获取到Class文件
3、 因为所有的设备对象都实现了PCI接口,因此可以利用Class获取到的空参对象强转为PCI类型
4、 将此PCI设备传进主板的使用PCI方法,即可。将流关闭
主函数:
1 | public static void main(String[] args) throws Exception { |
配置文件
pci1=cn.zc.reflect.test.SoundCard
pci2=cn.zc.reflect.test.NetCard
接口
1 | public interface PCI { |
主板
1 | public class Mainboard { |
反射操作注解
ORM:Object relationship Mapping,对象关系映射
让Java实体类与数据库表结构对应起来
1 | class Student{ |
| id | name | age |
|---|---|---|
| 001 | hugh | 20 |
| 002 | wang | 30 |
要求使用注解和反射完成类和表的结构对应关系
核心思路,自定义注解,要求传入列名,类型,长度,然后在POJO类及其字段上加上注解,这样通过反射获取到POJO类的Class对象,然后可以获取类上的注解,再获取属性然后获取属性上的注解
类名注解与属性注解如下
1 | //类名的注解 |
实体类如下
1 | ("db_stu") |
获取注解及值如下
1 | public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException { |
正则表达式
概述
正确表达的字符串。
正则表达式用于操作字符串数据。
通过一些特定的符号来体现的。
所以为了掌握正则表达式,必须要学习一些符号。
虽然简化了,但是阅读性差。
常见的规则
字符类
1 | [abc] 字符串某一位必须是a或b或c,只能是三者中一个 |
预定义字符类
1 | . 任何字符(与行结束可能匹配也可能不匹配) |
边界匹配器
1 | ^ 行的开头 |
Greedy数量词
1 | X? X,一次或一次也没有,至多一次 |
常见的功能
Pattern类指定为字符串的正则表达式必须首先被编译为此类的实例(将正则表达式封装成对象)。然后可将得到的模式(正则规则)用于创建Matcher对象(匹配器),依照正则表达式,该对象可以与任意字符序列匹配。执行匹配所涉及的所有状态都驻留在匹配器中,所以多个匹配器可以共享同一模式。
String类中这些关于正则的方法底层调用的是正则对象的方法,不过String类直接使用比较简单点。
1 | /* |
相关题目整理
基础
a++与++a区别
a=3 b = a++ 和b = ++a
若是单个语句,则没有太大区别,若是在运算当中,则
a++,先将a的值用temp存储,然后进行a = a+1,然后存下原来的temp,及若b = a++,
b = 3,a =4;若b = ++a,则b = 4,a = 4
若为i = 3,i = i++,则i= 3,因为存储的是temp的值,+=,-=,*=,/=.为左=左加右,左=左减右,以此类推
short s = 4 ,s += 4与s = s + 4的区别
s += 4为赋值运算,在底层做了自动的强制转换,编译成功,s = s + 4没有自动转换,精度损失,编译不会通过
两个数互换位置,不使用第三个数
1 | a = a ^ b |
用三元运算符取两个整数中大的
1 | int x,y |
if 与 switch的比较
if :
对具体的值进行判断。
对区间判断。
对运算结果是boolean类型的表达式进行判断。
switch
对具体的值进行判断
值的个数是固定的。
对于几个固定的值判断,建议使用switch进行判断,因为switch语句将具体的答案加载进内存,效率相对较高
while和for循环区别
for循环结束后,控制循环变量被释放,无法被再次利用,而while中的变量可以。如果不使用此变量,用for较好,节省内存空间;若要用到变量,则用while较好。
直接打印数组名
直接打印数组名出来的符号什么意思,如[I@c17164
@分隔符,右边c17164是在当前操作系统下计算出的数组地址,如windows下用哈希算法计算出来的地址位置,左边[表示数组,I表示Int类型
数组异常
使用数组带来的问题
1、 当访问到数组中不存在的角标,会出现ArrayIndexOutOfBoundsException
2、 当引用型变量没有任何实体指向时,还在用其操作,会出现NullPointerException
面对对象
如何理解面对对象
(1) 符合现在人们思维的习惯(2)使复杂的事情变得简单化(3)让我们从程序的执行者变成指挥者。面向对象的三个特征:封装,继承,多态
类与对象之间的关系
类:事物的描述
对象:该类事物的实例,在java中通过new来创建
构造函数与一般函数区别
构造函数对象创建时就会调用与之对应的构造函数对对象进行初始化,而一般函数对象创建后需要该函数功能时才调用;构造函数对象创建时,会调用只调用一次;一般函数对象创建后可以被调用多次。
什么时候定义构造函数
在描述事物时,该事物一存在就已具备的一些内容,这些内容定义在构造函数中。可以以重载形式进行运行,定义不同的构造函数
若有静态代码块、构造代码块、构造函数,三个执行顺序
静态代码块随着类的加载而执行,最先执行。如果有对象,构造代码块执行,然后构造函数执行。如果有继承,先运行父类构造函数。
抽象类
- 抽象类中有构造函数吗?
有,用于给子类对象进行初始化
- 抽象类可以不定义抽象方法吗?
可以,但是很少见,目的是不让该类创建对象。AWT的适配器对象就是这种类
通常这个类中的方法有方法体,但是却没有内容
- 抽象关键字不可以和哪些关键字共存?
private不行,抽象类需要被子类覆盖 ; static不行,抽象类本身不需创建对象
final 不行,抽象类需要被子类覆盖
- 抽象类和一般类的异同点
相同点:抽象类和一般类都是用来描述事物的,都在内部定义了成员。
不同点:
一般类有足够的信息描述事物。抽象类描述事物的信息有可能不足。
一般类中不能定义抽象方法,只能定义非抽象方法。抽象类中可以定义抽象方法,同时也可以定义非抽象方法。
一般类可以被实例化。抽象类不可以被实例化。
抽象类一定是父类吗?
是的,需要子类覆盖方法后才可以对子类实例化。
this关键字含义,final特点
this关键字:调用本类属性,调用本类方法,使用本类构造器,本类对象的引用
final:final是一个修饰符,可以修饰类,方法,变量;final修饰的类不可以被继承;final修饰的方法不可以被覆盖;final修饰的变量是一个常量,只能赋值一次变量命名与函数一样,常量所有字母都大写
main输出1
如果用单|,要判断左边跟右边,这时候j=0+4,但是如果是||,左边满足,那么右边直接被屏蔽,那么j仍然等于4。输出4.
String
String比较
1 | public static void stringDemo1(){ |
第一个为真,第二个为假。第一种创建String方式为在常量池中创建对象,可以共享,而new在堆内存中进行创建。
如果要比较对象的内容,用equals(),而String类将Object中的比较方法进行覆写,普通的equals()比较地址,而字符串比较内容。
结果一个为5,一个为41,原因下面将4变成了4字符串。第三种方式也可以实现输出41,更简洁。
字符串排序
{“nba”,abc”,”cba”,”zz”,”qq”,”haha”}
1 | 思路: |
一个子串在整串中出现的次数
“nbaernbatynbauinbaopnba”
1 | 思路: |
两个字符串中最大相同的子串
“qwerabcdtyuiop”
“xcabcdvbn”
继承
什么时候定义继承
当类与类之间存在着所属关系的时候,就定义继承。xxx是yyy的一种,xxx extends yyy
继承就是不断向上抽离的过程,如果A和B部分功能相同,可以将相同功能进行抽离,变成父类。
子类实例化访问父类构造函数原因
子类继承了父类,获取到了父类中的内容(属性),所以在使用父类内容之前要先看父类是如何对自己的内容进行初始化的,所以子类在构造对象时必须访问父类的构造函数。为了完成这个必须的动作,就在子类的构造函数函数中加入了super();语句.如果父类中没有定义空参数构造函数.那么子类的构造函数必须用super明确要调用父类中哪个构造函数同时子类构造函数中如果使用this()调用了本类构造函数时,super就没有了,因为this和super都只能定义在第一行,所以只能有一个。但是可以保证的是,子类中肯定会有其他的构造函数访问父类的构造函数。
main输出题目1
A 可以,因为覆盖了
B不可以,权限不够
C可以,子类特有方法
D 不可以,调用的不确定项
E不可以,静态只能覆盖静态
多态
main输出题目1
1 | class Fu{ |
main输出题目2
1 | class Fu{ |
main输出题目3
结果为A B
因为f.show()调用父类方法,被子类覆盖,输出A,然后进行for循环判断,输出B,返回false,因为为双与,因此直接短路,循环结束。所以总输出结果为A B。
main输出题目4
输出B C 7。使用多态,在子类的构造函数中第一行为super(),因此输出B,此时i=0,然后i+2=2,然后输出C,i+5=7。因此最后结果为B C 7。
main输出题目5
输出:4 5 showZi showZi
main输出题目6
执行子类构造函数,默认第一行为super(),因为父类中没有空参的构造函数,只有有参的构造函数,因此super()不能被执行,编译失败。
main输出题目7
因为父类中方法与此类不一样,覆盖失败,编译报错。调用的不确定性。
多态时成员的特点
1.成员变量
编译时:参考引用型变量所属的类中是否有调用的成员变量,有,编译通过;没有,编译失败。
运行时:参考引用型变量所属的类中是否有调用的成员变量,并运行所属类中的成员变量
简单说:编译和运行都参考等号的左边
2.成员函数(非静态,重点)
编译时:参考引用型变量所属的类中是否有调用的函数,有,编译通过;没有,编译失败。
运行时:参考的是对象所属的类中是否有调用的函数
简单说:编译看左边,运行看右边
3.静态函数
编译时:参考引用型变量所属的类中是否有调用的静态方法
运行时:参考引用型变量所属的类中是否有调用的静态方法
简单说:编译和运行都参考等号的左边
其实静态方法是不需要对象的,直接类名调用即可
内部类
内部类可以直接访问外部类中成员的原因
内部类持有外部类的引用,外部类名.this
从内部类在局部位置上只能访问局部中被final修饰的局部变量
main输出1
在主函数中,若直接new Inner()对象会失败,因为静态方法中不允许有非静态成员,相当于this.new Inner(),而static中不允许出现this,此时需要将class Inner修改成 static class Inner
main输出2
new Object(){}相当于创立了子类对象,而Object obj=子类对象,匿名内部类这个子类对象被向上转型(多态)为Object类型,隐藏了子类特有属性,这样就不能使用子类特有的方法。编译看左边,Object类中无show方法,因此会编译失败
main输出3
编译失败,因为内部类中如果定义了静态成员,该内部类必须被静态修饰,或者该成员变量被final修饰。因为内部类相当于外部类的成员,必须在外部类的对象创建以后进行,java虚拟机要求所有的静态变量要在对象创建之前完成,因为如果非静态内部类中有静态变量,就要先加载非静态方法,再加载静态成员,与JVM矛盾。
但是可以在非静态内部类中定义静态常量(静态常量一定要有一个编译期常量),如果变量被static final修饰,字面常量会在编译阶段确定,称为编译期常量,不需要加载类的字节码文件,即编译期常量不会导致类加载,因此静态常量在非静态内部类中是合法的。(编译期常量折叠:编译期在编译阶段通过语法分析计算出常量表达式的具体值)。但是如果将y改成Math.randm(),会报错,因为这个需要运行确定。
总结:非静态内部类中不能拥有静态成员变量/方法,但是可以有静态的编译期常量,不能使用非编译期常量。
main输出4
A正确,在外部类访问内部类,必须要建立内部类的对象。
B 错误,因为主函数是静态方法,只能调用静态成员,所以内部类必须是静态的。
C错误,格式错误,应该是new Demo().new Inner();
D 格式正确,但是要求内部类是静态的。
main输出5
调用show()方法,不能直接show(),因为是非静态的,需要对象调用,new Demo().show();
然后传一个匿名对象进去,实现func()方法。
接口
main输出1
编译失败,因为编译看左边,运行看右边,而因为接口A中没有func方法,所以编译会报错。a所属的A接口中没有func()方法。
main输出2
编译失败,A a = get();相当于A a = new B();相当于把B对象封装,因为A中没有test()方法,因此编译报错。
异常
main输出1
找main函数入口,执行show()方法,此方法进栈,抛出异常被catch,因此进入catch,输出B,finally一定会被执行,输出C,问题被解决了,输出D。因此结果为B C D。
main输出2
throw异常下面的语句无法被执行,因为抛出异常就进入catch捕获异常。因此输出A是句废话,编译失败。与main输出1不一样,main输出1将异常封装,方法下面的语句还有可能被执行到。
throw 语句下面不要加其他语句,必然要跳转!!!
main输出3
多catch时,父类的catch放在最下面,因此编译会失败。
main输出4
134
13423
原因:foo(0),不等于1,Output=1,然后执行finally,output=13,正常结束,output=134,然后输出134;执行foo(1),一次是满足条件,丢出异常,output=1342(因为Output为静态变量,一直存在,因此始终操作的是同一变量),然后执行return,但是因为finally一定被执行(除非退出jvm),因此output=13423
多线程
实现死锁
死锁的代码:利用同步的嵌套
run方法中封装线程任务,设置两个同步的嵌套,一个为同步锁a,b;另一个为同步锁b,a;然后设定同步锁对象。
1 | class Test implements Runnable{ |
面试题1
错误在第一行,此run()方法为子类特有方法,Test实现了Runnable接口,但是没有覆盖Run()方法,应该被abstract修饰
面试题2
判断Thread自身run()和runnable的run方法优先级,应该输出subThread run。
Thread实现了自己的run方法,因此直接执行下边的run()方法,只有在下边的run()未实现时,才会寻找到上边的Runnable的run方法,并执行,这就是优先级顺序。
因为应该以子类的任务为主(new Thread中的run方法),如果子类没有覆写run方法,那么则以任务对象为主,如果没有任务对象,则以Thread原有run方法为主