Java进阶
- idea和eclipse
- eclipse的快速生成
- main函数:main
- 输出语句:syso
- idea的快速生成
- main函数:psvm
- 输出语句:sout
- 在idea中一个project相当于eclipse当中的一个workspace,在空的工程下新建Module(模块),IDEA中模块类似于eclipse当中的project
- eclipse的组织方式:workspace->project
- idea的组织方式:project->module
- idea的字体设置:
- file->settings->输入font->设置字体样式以及字号大小
- idea是自动保存,不需要ctrl+s
- idea添加构造方法/setXxx/getXxx:alt+insert
- eclipse的快速生成
- 抽象类
- 什么是抽象类?
- 类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类,类本身是不存在的,所以抽象类无法创建对象,无法实例化。
- 抽象类属于什么数据类型?
- 抽象类也属于引用数据类型,编译之后也是一个class字节码文件
- 抽象类怎么定义?
- [修饰符列表] abstract class 类名{
- 类体;
- }
- [修饰符列表] abstract class 类名{
- 抽象类是无法实例化的,无法创建对象,所以抽象类都是用来被子类继承的,所以final和abstract不能联合使用,这两个关键字是对立的
- 抽象类的子类可以是抽象类
- 抽象类虽然无法实例化,但抽象类有构造方法,这个构造方法供子类使用
- 抽象类关联到一个概念,抽象方法,什么是抽象方法呢?
- 抽象方法只有方法的声明,没有方法体,以分号结尾
- public abstract void doSome();
- 抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中
- 重要结论:
- 一个非抽象的类继承抽象类,必须将抽象类中的抽象方法重写
- 一个抽象的类继承抽象类,可以不去将抽象类中的抽象方法重写
- 抽象类和非抽象之间可以使用多态,面向OCP原则
- 面试题:Java语言中凡是没有方法体的方法都是抽象方法
- 不对,错误的
- Object类中就有很多方法没有方法体,都是以;结尾的,但他们都不是抽象方法,例如:
- public native int hashCode();
- 这个方法底层调用了C++写的动态链接库程序
- 前面修饰符列表中没有abstract,有一个native,表示调用JVM本地程序
- 什么是抽象类?
- 接口
- 接口也是一种引用数据类型,编译之后也是一个class字节码文件
- 接口是完全抽象的,(抽象类是半抽象的),或者也可以说接口是特殊的抽象类,特殊在他是完全抽象的
- 接口怎么定义,语法是什么?
- [修饰符列表] interface 接口名{}
- 接口支持多继承,一个接口可以继承多个接口
- 接口中只包含两部分内容,一部分是常量,一部分是抽象方法,接口中的所有元素都是public修饰的
- 接口中的抽象方法定义时,public abstract修饰符可以省略
- 接口中的常量,public static final可以省略
- 类和类是继承(单继承),接口和接口是继承(多继承),类和接口之间是实现implements(多实现)
- 当一个非抽象类实现接口,必须将接口中的所有抽象方法全部实现
- 接口和非抽象之间可以使用多态,面向OCP原则
- 继承和实现都存在的话,extends关键字在前,implements关键字在后,如果没写继承,则默认继承Object类
- 接口在开发中的作用:
- 注意:接口在开发中的作用类似于多态在开发中的作用:
- 多态:面向抽象编程(将Dog/Cat写成Animal),不要面向具体编程,降低程序的耦合度,提高程序的扩展力、
- 总结:面向接口编程,可以降低程序的耦合度,提高程序的扩展力
- 接口离不开多态,接口+多态才可以达到降低程序的耦合度,提高程序的扩展力
- 类型和类型之间的关系:
- is a
- Cat is a Animal(猫是一个动物)
- 凡是能够满足is a的表示“继承关系”
- A extends B
- has a
- I have a Pen(我有一只笔)
- 凡是能够满足has a关系的表示“关联关系”
- 关联关系通常以“属性的形式存在”
- A{
- B b;
- }
- like a:
- Cooker like a FoodMenu(厨师像一个菜单一样)
- 凡是能够满足like a关系的表示“实现关系”
- 实现关系通常是:类实现接口
- A implements B
- is a
- 抽象类和接口的区别:
- 抽象类是半抽象的
- 接口时完全抽象的
- 抽象类中有构造方法
- 接口中没有构造方法
- 类之间是单继承
- 接口之间可以多继承
- 类可以实现多个接口
- 接口中只允许出现常量和抽象方法
- Object类中的方法在哪找?
- 第一:去源代码当中(但是这种方式比较麻烦,因为源代码也比较难)
- 第二: 去查阅java的类库的帮助文档
- 什么是API(Application Program Interface)?
- 应用程序编程接口
- 整个JDK类库就是一个javase的API
- 每一个API都会配置一套API帮助文档
- Object类需要了解的方法:
- Object的toString方法
- public String toString() {
- return getClass().getName() + "@" + Integer.toHexString(hashCode());
- }
- 返回的是:类名@对象的内存地址通过哈希算法转换为十六进制的形式
- SUN公司设计toString方法的目的?
- toString方法的设计目的是:通过调用这个方法可以将一个“Java对象”转换成字符串形式
- SUN公司开发Java语言的时候,建议所有的子类都去重写toString方法
- 输出引用的时候。默认会调用引用的toString方法
- public String toString() {
- Object的equals方法
- public boolean equals(Object obj){
- return (this == obj);
- }
- 以后编程的过程当中,都要通过equals方法来判断两个对象是否相等
- 判断基本数据类型是否相等直接使用“==”就可以,==判断的是值的大小,所以引用数据类型不能用“==”判断
- 在Object类的equals方法中,默认采用的是“==”判断两个Java对象是否相等,而“==”判断的是两个对象的内存地址是否相等,所以老祖宗的equals方法不够用,需要重写equals方法
-
改良之后的代码
-
再次改良之后的代码
- 判断基本数据类型的数据用==,判断引用数据类型的数据用equals
- public boolean equals(Object obj){
-
String类
- String类已经重写了equals方法,比较两个字符串相等不能使用==,必须使用equals
- String类已经重写了toString方法
- Object的finalize方法
- protected void finalize() throws Throwable { }
- finalize()方法有一个没有代码的方法体,而且这个方法是protected修饰的
- 这个方法不需要程序员手动调用,JVM的垃圾回收器负责调用这个方法,不像equals,toString方法是需要写代码调用的,finalize方法只需要重写,重写完将来自动会有程序来调用
- 当一个Java对象即将被垃圾回收器回收的时候,垃圾回收器负责调用finalize()方法
- finalize()方法实际上是SUN公司为Java程序员准备的一个时机,垃圾销毁时机,如果希望在对象销毁时机执行一段代码的话,这段代码要写到finalize()方法当中
- java中的垃圾回收器不是轻易启动的,垃圾太少,或者时间没到,种种条件下,又可能启动,也有可能不启动
- 有一段代码可以建议垃圾回收器启动:System.gc();,只是建议,可能启动,也可能不启动,只是启动的概率高了一些
- Object的hashCode方法
- public native int hashCode();
- 这个方法不是抽象方法,带有native关键字,底层调用C++程序
- hashCode方法返回的是哈希码
- 实际上就是一个Java对象的内存地址,经过哈希算法,得出的一个值
- 所以hashCode方法的执行结果可以等同看做一个Java对象的内存地址
- 内部类:
- 在类的内部又定义了一个新的类。被称为内部类
- 分类:
- 实例内部类:类似于实例变量
- 静态内部类:类似于静态变量
- 局部内部类:类似于局部变量
- 不能使用访问权限修饰符和static修饰
- 匿名内部类:
- 匿名内部类是一种特殊的局部内部类,属于局部内部类的一种,它是通过匿名类实现接口
- 学习匿名内部类主要是以后阅读别人的代码时,可以理解,并不代表以后都要这样写,因为匿名内部类有两个缺点:
- 缺点一:太复杂、太乱、可读性差
- 缺点二:类没有名字,以后想重复使用,不能用
- 匿名内部类例子:
- 内部类的修饰符:private、protected、public及默认修饰符
- dosome方法中的局部内部类,在其他方法内不能使用
- 使用内部类编写的代码可读性很差,能不用尽量不用
- 数组( 数组没有对应的类文件)
- 一维数组:
- 数组是一种引用数据类型,不属于基本数据类型,父类也是Object
- 数组实际上是一个容器,可以同时容纳多个元素
- 数组当中可以存储“基本数据”类型的数据,也可以存储“引用数据类型”的数据
- 数组因为是引用数据类型,所以数组对象在堆内存当中
-
数组当中如果存储的是java对象的话,实际存储的是对象的“引用(内存地址)”
- 数组一旦创建,长度不可变
- 数组的分类:一维数组、二位数组、三维数组、多维数组...........
- 所有的数组对象都有length属性,用来获取数组中元素的个数
- Java中的数组要求数组中元素的类型统一,比如int类型数组只能存储int类型,Person类型数组只能存储Person类型
- 数组在内存方面存储的时候,数组中的元素内存地址是连续的,这是数组存储元素的特色,数组实际上是一种简单的数据结构
- 所有数组都是拿第一个小方框的内存地址作为整个数组的内存地址,因为这样就可以算出后面数组的内存地址
- 数组中每一个元素都是有下标的,下标从0开始,以1递增,最后一个数组的下标是length-1
- 数组数据结构的优点和缺点?
- 优点:
- 查询/查找/检索某个下标上的元素时效率极高,可以说查询效率最高的一个数据结构
- 为什么检索效率高?
- 第一:数组中元素的内存地址在空间存储上是连续的
- 第二:每一个元素类型相同,所以占用空间大小一样
- 对于基本数据类型,保存的是数据,每个元素占用空间大小相同,对于引用数据类型,数组保存的是内存地址,而内存地址占用的空间大小是一样的
- 第三:知道第一个元素的内存地址,有知道下标,所以通过一个数学表达式就可以某个下标上元素的内存地址,然后直接通过内存地址定位元素,所以数组的检索效率最高
- 数组中存储100个元素,或者100万个元素,在元素检索方面,效率是一样的,因为数组中的元素查找的时候,不会一个一个找,是通过数学表达式计算出来的(算出一个内存地址,直接定位)
- 查询/查找/检索某个下标上的元素时效率极高,可以说查询效率最高的一个数据结构
- 缺点:
- 第一:由于为了保证数组中每个元素的内存地址连续,所以在数组上随机删除或者增加元素的时候,效率较低,因为随机增删元素会涉及到后面元素统一向前或向后移的操作
- 第二:数组不能存储大数据量,因为很难在内存空间上找到一快特别大的内存空间
- 注意:对于数组中最后一个元素的增删,是没有效率影响的
- 优点:
- 怎么声明/定义一个一维数组?
- int[] array1;或int ayyar1[];//不建议这种,这是C++风格
- double[] array2;
- String[] array3;
- Object[] array4;
- 怎么初始化一个一维数组?
- 包括两种方式:静态初始化一维数组、动态初始化一维数组
- 静态初始化一维数组(定义的同时初始化):
- int[] array={1,2,,3,4,5};
- int[] array=new int[]{1,2,3,4,5};
- 动态初始化一维数组(先定义,后初始化):
- int[] array=new int[5];//5表示数组元素的个数,每个元素默认值0
- 什么时候采用静态初始化方式,什么时候采用动态初始化方式?
- 当创建数组的时候,确定数组中存储哪些具体的元素时,采用静态初始化方式
- 当创建数组的时候,不确定数组中存储哪些元素时,采用动态初始化方式来预先分配内存空间、
-
- 对于静态初始化,上面这种方式不能,但可以int[] array={1,2,3,4};然后将array作为实参传进去或者形参写成new int[]{1,2,3,4}
-
public static void main(String[] args){}
-
System.out.println(args.length);//值是0,代表数组创建了只是没有任何数据,args并不是null
- 其实这个数组是留给用户的,用户可以在控制台输入参数
- 例如这样运行程序:java ArrayTest abc def xyz
- 那么这个时候JVM自动将”abc def xyz“通过空格的方式进行分离,分离完成之后,自动放到“String[] args”数组中
- 把abc def xyz 转换成字符串数组{“abc”,“def”,“xyz”}
-
-
数组的扩容:
- 在Java开发中,数组长度一旦确定不可变,那么数组满了怎么办?
- 数组满了,需要扩容
- Java中对数组的扩容:先建一个大容量的数组,然后将小容量的数组中的数据一个一个拷贝到大数组当中
- 结论:数组扩容效率较低,因为涉及到拷贝的问题,所以在以后的开发中请注意,尽可能少的进行数组的拷贝,可以在创建对象的时候预估计多少合适,最好预估准确,这样可以减少数组的扩容次数,提高效率
-
- 对象拷贝的是对象的内存地址
-
二维数组:
- 二维数组其实是一个特殊的一维数组,特殊在这个一维数组当中的每一个元素是一个一维数组
- 三维数组其实是一个二维数组,特殊在这个二维数组当中的每一个元素是一个一维数组
- 二维数组的静态初始化:
- int[][] a={{1,2,3,},{4},{5,6,7}};
- int[][] array=new int[][]{{1,2,3,},{4},{5,6,7}};
-
二维数组的动态初始化:
- int array[][]=new int[3][4];
- 二维数组其实是一个特殊的一维数组,特殊在这个一维数组当中的每一个元素是一个一维数组
- 常见的算法:
- 排序算法:冒泡排序、选择排序.......
- 查找算法:二分法查找.........
- 以上的算法其实在Java中已经封装好了,直接调用就可以,只不过面试可能会碰上
- 算法在Java中不需要精通,Java已经封装好,要排序调用方法就行,例如:Java中提供了一个数组工具类:
- java.util.Arrays;//Arrays是一个数组工具类,其中sort()方法可以排序,是静态方法,直接使用类名调用就行
- 冒泡排序:
- //5 3 2
- //3 5 2
- //3 2 5
- //2 3
-
选择排序:
- 选择排序比冒泡排序的效率高,高在交换位置的次数上,选择排序的交换位置是有意义的
- 循环一次,然后找出参加比较的这堆数据中最小的,拿着这个最小的值和最前面的数据交换位置
-
选择排序和冒泡排序的比较次数没变,但是交换次数明显减少了
-
二分法查找:二分法查找要建立在排序的基础之上
-
二分法查找效率要高于“一个挨着一个”的这种查找方式
-
二分法查找算法的终止条件:一直折半,直到中间的那个元素恰好是被查找的元素
-
Arrays工具类:
- Arrays是SUN公司为程序员已经写好的一个工具类,所有方法都是静态的,直接类名调用
- java.util.Arrays
- int[] arr={10,21,30,5,3,8,19,22};
- Arrays.binarySearch(arr,22);//用来对数组进行二分法查找
- Arrays.sort(arr);//用来对数组进行排序
- 主要使用两个方法:查找和排序
- 一维数组:
- String类
-
无论String a=“abc”;还是String a=new String("abc");a中都保存的是内存地址
-
所以字符串比较的时候不能用双等号,双等号不保险 ,String类已经重写了equals方法,所以应该调用String类的equals方法
-
String a="abc", a.equals("abc"); 和 "abc".equals(a) 建议使用 "abc".equals(a),可以避免空指针异常
- 关于String类的常用构造方法:
-
String类的常用方法:
- charAt方法:
-
compareTo方法:
-
contains方法
-
endsWith方法
- startWith方法
-
equals方法
-
-
-
equalsIgnoreCase方法
- getBytes方法
- indexOf方法
-
lastIndexOf方法
-
isEmpty方法:length长度为0,则为空,
- 判断数组长度和字符串长度不一样:
- 判断数组长度是length属性
- 判断字符串长度是length方法
- replace方法
-
split方法
-
substring方法
-
toCharArray方法
-
toLowerCase方法
-
toUpperCase方法
-
trim方法
-
valueOf方法(String中只有这一个方法是静态的,不需要new对象)
- printin()方法在底层都会调用String.valueOf()
-
-
StringBuffer类
- 思考:我们在实际开发中,如果需要进行字符串的频繁拼接,会有什么问题?
- 因为java中的字符串是不可变的,每一次拼接都会产生新的字符串
- 这样会占用大量的方法区内存,造成内存空间的浪费
- String s="abc";
- s+="hello"; 就以上两行代码,就导致在方法区字符串常量池中创建了3个对象:
- "abc"
- "hello"
- "abchello"
- 如果以后需要进行大量的字符串拼接操作,建议使用JDK中自带的:
- java.lang.StringBuffer
- java.lang.StringBuilder
- 如何优化StringBuffer的性能?
- 在创建StringBuffer的时候尽可能给定一个初始化容量
- 最好减少底层数组的扩容次数(因为每扩容一次底层就会调用System.arraycopy方法),预估计一下,给一个大一点些的初始化容量
- String不可变:
- StringBuffer可变:
-
StringBuffer和StringBuilder区别:
- StringBuffer中的方法都有:synchronizaed关键字进行修饰,表示StringBuffer在多线程环境下是安全的
- StringBuilder中的方法都没有:synchronizaed关键字进行修饰,表示StringBuilder在多线程环境下运行是不安全的
- 面试题:
- String为什么是不可变的?
- 我看过源代码,String类中有一个byte[]数组,这个byte[]数组采用了final修饰,因为数组一旦创建长度不可变,并且被final修饰的引用一旦指向某个对象之后,不可再指向其他对象,所以String是不可变的!
- jdk1.8及以前String使用的是char数组,jdk1.9及以后使用的是byte数组。
- “abc”无法变成“abcd”
- 我看过源代码,String类中有一个byte[]数组,这个byte[]数组采用了final修饰,因为数组一旦创建长度不可变,并且被final修饰的引用一旦指向某个对象之后,不可再指向其他对象,所以String是不可变的!
- StringBuilder和StringBuffer为什么是可变的呢?
- 我看过源代码,StringBuffer和StringBuilder内部实际上是一个byte[]数组,这个byte[]数组没有被final修饰,StringBuffer和StringBuilder的初始化容量我记得应该是16,当存满之后,底层调用了数组拷贝的方法,System.arrayCopy()方法,是这样扩容的,所以StringBuilder和StringBuffer适合于使用字符串的频繁拼接
- String为什么是不可变的?
- 思考:我们在实际开发中,如果需要进行字符串的频繁拼接,会有什么问题?
- 包装类:
- 装箱和拆箱:
- 装箱:将基本数据类型转换为包装器类型;
- 拆箱:将包装器类型转换为基本数据类型
- 构造方法
- 访问最大值和最小值
-
自动装箱和拆箱
-
- ==这个运算符不会触发自动拆箱机制,只有+ - * /等运算的时候才会触发
- 数字格式化异常:
-
字符串转换为int类型
- 装箱和拆箱:
-
java对日期的处理
-
- 注意:字符串的格式的日期格式和SimpleDateFormat对象指定的日期格式要一样,不然会出现异常
-
获取自1970年1月1日 00:00:00 000到当前系统的总毫秒数
-
简单总结一下System类相关属性和方法:
-
- System.out【out是System类的静态变量】
- System.out.println()【println()方法不是System类的,是PrintStream类的方法】
- System.gc()【建议启动垃圾回收器】
- System.currentTimeMillis【获取自1970年1月1日 00:00:00 000到当前系统的总毫秒数】
- System.exit(0)【退出JVM】
- System.arraycopy(.....)【数组的扩容,拷贝数组】
-
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
-
-
- 数字格式化:
-
BigDecimal
-
Random
-
枚举
-
思考:以上的这个方法设计没毛病,挺好,返回true和false两种情况,但是在以后的开发中,有可能遇到一个方法的执行结果可能包括三种情况,四种情况,五种情况等等,但是每一种情况都是可以数清楚的,一枚一枚列举出来的,这个布尔类型就无法满足了,此时需要使用Java中的枚举类型
-
枚举:一枚一枚列举出来的,才建议使用枚举类型
- 枚举编译之后也是生成class文件
- 枚举也是一种引用数据类型
- 枚举中的每一个值可以看作是常量
- 枚举怎么定义:
- enum 枚举类型名{
- 枚举值1,枚举值2;
- }
- enum 枚举类型名{
- 结果只有两种情况的建议使用布尔类型,结果超过2种并且还是可以一枚一枚列举出来的,建议使用枚举
- 例如:颜色、四季、星期
- switch在高版本也支持枚举
-
异常:
- UML(统一建模语言):
- 画UML图的工具有很多,例如Rational Rose(收费的)、starUML等...
- 什么是UML?有什么用?
- UML是一种统一建模语言
- 一种图标式语言(画图的)
- UML不是只有java中使用,只要是面向对象的编程语言,都有UML
- 一般画UML图的都是软件架构师/系统分析师,这些级别的人员使用的
- 在UML图中可以描述类和类之间的关系,程序执行的流程,对象的状态等
- 在Java软件开发当中,java软件开发人员必须能看懂
-
java异常的结构图:
-
错误分为:语法错误、运行错误、逻辑错误
- 语法错误:
- 是指由于编程中输入不符合语法规则而产生的。程序编译就通不过,程序不能运行起来。此类错误最简单,调试起来比较容易。
- 表达式不完整、缺少必要的标点符号、关键字输入错误、数据类型不匹配、循环语句或选择语句的关键字不匹配等。通常,编译器对程序进行编译的过程中,会把检测到的语法错误以提示的方式列举出来,又称为编译错误。
- 运行错误:
- 指程序在运行过程中出现的错误。程序通过语法错误检测,但是运行的时候出现错误,导致程序被迫终止,此类错误有特定的发生条件,因此能够准确的定位错误代码段,因而调试也比较方便。
- 除法运算时除数为0 、数组下标越界、文件打不开、磁盘空间不够、数据库连接错误等。
- 逻辑错误:
- 程序运行后,没有得到设计者预期的结果,这就说明程序存在逻辑错误。这种错误在语法上是有效的,但是在逻辑上是错误的。
- 使用了不正确的变量,指令的次序错误,循环的条件不正确,程序设计的算法考虑不周全等。
- 语法错误:
-
编译时异常又被称为受检异常/受控异常,运行时异常又被称为未受检异常/未受控异常
编译时异常和运行时异常,都是在运行阶段发生的,因为程序只有在运行阶段才可以new对象,异常的发生就是new异常对象,编译阶段异常是不会发生的,
-
编译时异常为什么而得名?
- 因为编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器报错,而得名
- 编译时异常和运行时异常区别:
- 编译时异常一般发生的概率比较高
- 举个例子:
- 你看到外面下雨,倾盆大雨
- 你出门之前会预料到:如果不打伞,我可能会生病(生病是一种异常)
- 而且这种异常的概率很高,所以我们出门之前要拿一把伞
- 拿一把伞就是对生病异常的发生之前的一种处理方式
- 举个例子:
- 运行时异常一般发生的概率比较低
- 举个例子
- 小明走在街上,可能会被天上的飞机轮子砸到
- 被飞机轮子砸到也算一种异常,
- 但是这种异常发生概率低
- 在出门之前你没必要对这种发生概率较低的异常进行预处理
- 如果你预处理这种异常,你将活得很累
- 举个例子
- 假设Java中没有对异常进行划分,没有分为,编译时异常和运行时异常,所有的异常都需要在编写程序阶段进行预处理,将是怎样的效果呢?
- 首先,这样的话,程序的绝对安全的,但是程序员编写代码太累,到处都是处理异常的代码
- 编译时异常一般发生的概率比较高
- Java语言对异常的处理方式包括两种:
- 第一种方式:在方法的声明位置使用throws关键字
- 异常发生之后,如果一直上抛,最终抛给了main方法,main方法继续向上抛,抛给了调用者JVM,JVM知道这个异常发生了,只有一个结果,终止java程序
- 第二种方式:使用try...catch方式进行异常的捕捉
- 第一种方式:在方法的声明位置使用throws关键字
-
异常处理方式代码:
- 向上抛:
-
try...catch
- 向上抛:
-
注意:
-
只要异常没有捕捉,采用上报的方式,后续的代码就不会执行
-
另外需要注意,try语句块中的某一行出现异常,该行后面的代码不会执行
- try..catch捕捉异常之后,后续的代码可以执行
-
- try..catch深入
- catch小括号中的类型,可以是具体的异常类型,也可以是异常类型的父类型
- catch可以写多个,建议catch的时候,精确的一个一个处理,这样有利于程序的调试
-
catch写多个的时候,从上到下,必须从小到大
- JDK8的新特性:
-
异常对象有两个非常重要的方法:
- 获取异常简单的描述信息
- String msg=exception.getMessage();
- 打印异常追踪信息
- exception.printStackTrace();
- java后台打印异常堆栈追踪信息采用了异步线程的方式
- 获取异常简单的描述信息
- 发生异常和打印堆栈信息
- 发生异常
- 打印堆栈信息
- 发生异常
-
我们以后查看异常追踪信息,应该怎么看,可以快速调试程序?
- 异常追踪信息,从上往下一行一行看
- 但需要注意,SUN公司写的代码就不用看了(看包名就知道是自己的还是SUN的),主要问题是出现在自己编写的代码上
- finally
- 补充:如果初始化的语句用在try块中或if块中,也必须要让它在第一次使用前一定能够得到赋值。也就是说,把初始化语句放在只有if块的条件判断语句中编译器也会抗议,因为执行的时候可能不符合if后面的判断条件,如此一来初始化语句就不会被执行了,这就违反了局部变量使用前必须初始化的规定。但如果在else块中也有初始化语句,就可以通过编译,因为无论如何,总有至少一条初始化语句会被执行,不会发生使用前未被初始化的事情。对于try-catch也是一样,如果只有在try块里才有初始化语句,编译不通过。如果在catch或finally里也有,则可以通过编译。
- 发生异常后,引用就赋不上值,就为空
- 总之:放在finally中的代码是一定会执行的,但是try中如果是System.exit(0),finally就不执行了
- 自定义异常类:
- finalize()是什么?
- 垃圾回收机器(Garbage Collection),也叫GG,垃圾回收器主要有以下特点:
-
1. 当对象不再被程序所使用的时候,垃圾回收器将会将其回收
-
2. 垃圾回收是在后台运行的,我们无法命令垃圾回收器GC马上回收资源,但是我们可以告诉他可以尽快回收资源(System.gc()和Runtime.getRuntime().gc())
-
3. 垃圾回收器在回收某个对象的时候,首先会调用该对象的finalize()方法
-
4. GC主要针对堆内存
-
final、finalize、finally的区别?
- final是一个关键字,表示最终的,不可变的
- finally也是一个关键字,和try联合使用,使用在异常处理机制中,finally语句块中的代码是一定会执行的
- finalize()是Object类中的一个方法,作为方法名出现,所以finalize是标识符,这个方法是由垃圾回收器GC负责调用的,对象一旦调用了finalize方法就表示标记为可回收的,之后会被垃圾回收器回收
- throws和throw区别
- throws在方法的声明位置上使用,表示上报异常给声明者
- throw手动抛出异常
- 集合
- 什么是集合?有什么用?
- 集合实际上就是一个容器,可以来容纳其他类型的数据,但数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型
- 集合为什么说在开发中使用较多?
- 集合是一个容器,是一个载体,可以一次容纳多个对象,在实际开发中,假设连接数据库,数据库当中有10条数据,那么假设把这10条记录查询出来,在Java程序中会将10条数据封装成10个Java对象,然后将10个Java对象放到某一个集合当中,将集合传到前端,然后遍历集合,将数据一个个展现出来
- 集合不能直接存储基本数据类型的数据,另外集合也不能直接存储java对象,集合当中存储的是java对象的内存地址(或者说,集合存储的是引用)
- list.add(100);//其实是自动装箱Integer
- 注意:
- 集合在java中本身是一个容器,是一个对象
- 集合中任何时候存储的都是引用
- 数组和集合的区别:
- 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型
- 当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端, 因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。
- 集合的内存空间可以连续也可以不连续,元素类型可以相同也可以不同
-
在java中每一个不同的集合,底层会对应不同的数据结构,往不同的集合中存储元素,等于将数据放到了不同的数据结构当中,什么是数据结构? 数据存储的结构就是数据结构,不同的数据结构,存储方式不同,例如:数组、二叉树、链表、哈希表等等
- 在Java集合这一章,需要掌握的不是精通数据结合,Java中已经将数据结构实现了,已经写好了常用的集合类,你只需要掌握怎么用?在什么情况下选择哪一种集合去使用即可
- 集合在java.util.*;下,所有的集合类和集合接口都在java.util包下
- java集合分为两大类:
- 一类是单个方式存储元素:
- 单个方式存储元素,这一类集合中的超级父接口:java.util.Collection
- 另一类是键值对儿的方式存储元素:
- 以键值对的方式存储元素,这一类集合中的超级父接口:java.util.Map
- 一类是单个方式存储元素:
- 集合的继承结构图
-
- 这里列举的List和Set只是常用的,还有其他的
- 类之间的关系
- 依赖:一个类作为另一个类的返回值类型,形参类型,局部变量类型(虚线箭头)
- 关联:一个类作为另一个类的成员变量类型 (实线箭头)
- 继承:子类和父类之间,子接口和父接口之间(实线空心三角)
- 实现:子类和接口之间(虚线空心三角)
-
总结:
- ArrayList:底层是数组
- ArrayList初始化容量是10,扩容是增长到原来的1.5倍,原来时4,现在就是6(4+4>>1=6)
- 但是底层先创建了一个容量为0 的数组,当添加第一个元素的时候,初始化容量为10(优化了)
- ArrayList底层是Object类型的数组Object[]
- ArrayList集合因为底层是数组,所以尽可能少的扩容,因为数组扩容效率比较低,因为数组一旦创建长度不可变,数组扩容必须创建一个新的数组,然后进行拷贝,建议使用ArrayList集合的时候预估计元素的个数,给定一个初始化容量
- 数组优点是检索效率比较高,缺点是随机增删效率低,并且数组无法存储大数据量的元素,因为很难找到一块非常巨大的连续的内存空间,但向数组末尾增删元素效率很高,不受影响,检索效率高是因为每个元素占用空间大小相同,内存地址是连续的,知道首元素地址,知道下标,通过数学表达式计算出元素的内存地址,所以检索效率高
- 面试题:这么多集合中,你用哪个最多?
- 答:ArrayList集合
- 因为数组末尾添加元素,效率不受影响,一般也都是在末尾加元素
- 另外,我们检索/查找某个元素的操作比较多,数组检索效率高
- ArrayList初始化容量是10,扩容是增长到原来的1.5倍,原来时4,现在就是6(4+4>>1=6)
- LinkedList:底层是双向链表
- 链表的优点:由于链表上的元素在空间存储上内存地址不连续,所以随机增删元素的时候不会有大量元素位移,因此随机增删效率效率较高,在以后的开发中,如果遇到随机增删集合中的元素的业务比较多的时候,建议使用LinkedList
- 链表的缺点:不能通过数学表达式计算被查找的内存地址,每一次查找都是从头节点开始遍历,直到找到为止,所以LinkedList集合检索/查找效率较低
- 链表没有初始化容量
- 补充:一般集合添加元素都是在末尾添加,所以ArrayList用的多,而且ArrayList集合是非线程安全的,所以效率高
- Vector:底层是数组,线程安全的,效率低,使用较少
- HashSet:底层是HahMap,放到HashSet集合中的元素,等同于放到HashMap集合的key部分了
- TreeSet:底层是TreeMap,放到TreeSet集合中的元素,等同于放到TreeMap集合key部分了
- HashMap:底层是哈希表
- Hashtable:底层是哈希表,只不过是线程安全的,效率较低,很少使用
- Properties:是线程安全的,并且key和value只能存储字符串String
- TreeMap:底层是二叉树,TreeMap集合的key可以自动按照大小顺序排序
- ArrayList:底层是数组
-
- 关于java.util.Collection接口中的常用方法
-
迭代器:
-
-
集合对象只要发生改变(增加/删除/修改),迭代器必须重新获取
- 当集合结构发生了改变,迭代器没有重新获取时,调用next方法时:java.util.ConcurrentModificationException
-
- 迭代器删除元素和集合删除元素区别
- 集合删除元素
- 迭代器删除元素
- 结论:在迭代元素的过程中,一定要使用迭代器Iterator的remove方法,删除元素,不要使用集合自带的remove方法删除元素
- 获取集合迭代器对象,迭代器用来遍历集合,此时相当于当前集合的状态拍了一个快照,迭代器迭代的时候会参照这个快照进行迭代,当用集合自带的remove方法删除元素,和快照对比发现不一样,再调用next方法时就会出现异常,而迭代器的remove会将快照也进行改变,所以可以
- boolean contains(Object o)
- 是判断集合是否包含某个元素,包含返回true,不包含返回false
- contains方法底层是怎么判断集合中包含某个元素呢?
- 调用了equals方法进行比对
- equals方法返回true,就表示包含这个元素
-
结论:所以放在集合中的元素需要重写equals方法
-
boolean remove(Object o)
-
删除集合中的某个元素
-
remove底层也调用了equals方法
-
-
- List集合特有方法:
- List集合存储元素特点:有序、可重复
- 有序:List集合中元素有下标,从0开始,以一递增
- 可重复:存储一个1,还可以再存储1
- List既然是Collection接口的子接口,那么肯定list接口有自己“特色”的方法(以下只列出常用的方法)
- void add(int index,Object element)
- Object get(int index):List集合有自己特有的遍历方式,根据for循环和get方法就可以得到元素
- int indexOf(Object o)
- int lastIndexOf(Object o)
-
E remove(int index)
- Object set(int index,Object element)
- List集合存储元素特点:有序、可重复
-
计算机英语增删改查这几个单词要知道:
-
增:add、save、new
- 删:delete、drop、remove
- 改:update、set、modify
- 查:find、get、query、select
-
- ArrayList的构造方法:
-
链表数据结构:
- 对于链表数据结构来说,基本的单元是节点Node
- 对于单向数据链表来说,任何一个节点Node都有两个属性:
- 第一:存储的数据 第二:下一节点的内存地址
- 链表的内存地址可以是不连续的
- 链表随机增删效率高,因为不涉及到大量元素的位移问题
- 链表查询效率低,因为每次都要从头节点开始寻找
-
双向链表源码分析:
-
Vector
- 底层也是一个数组
- 初始化容量:10
- 扩容是原来的2倍
- 10——>20——>40
- Vector中所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的,效率比较低,使用较少了
- 怎么将一个线程不安全的ArrayList集合转换成线程安全的呢?
- 使用集合工具类:
- java.util.Collections;
- java.util.Collection 是集合接口
- java.util.Collections 是集合工具类
- 使用集合工具类:
-
泛型(JDK5.0之后,推出泛型机制)
- 泛型的好处:
- 第一:集合中存储的元素类型统一了
- 第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的向下转型
- 泛型的缺点
- 导致集合中存储的元素缺乏多样性,但大多数业务中,集合中元素的类型还是统一的,所以泛型特性被大家认可
- A instanceof B表示的含义:A通常是引用变量,B通常是一个类或者是接口
- A是否是B的实例
- A是否是B子类的实例
- A是否是B接口实现类的实例
-
JDK8之后引入了:自动类型推断机制(又称为钻石表达式)
- 自定义泛型
- 泛型的好处:
-
增强for循环(foreach)
-
hashSet集合
-
SortedSet集合:
-
Map集合
-
Map遍历方式
- 第一种方式,获取所有的key,通过遍历key,来遍历value
-
第二种方式:Set<Map.Entry<K,V>> entrySet()将Map集合转换为Set集合
-
总结:第二种方式效率高,因为获取key和value都是直接从node对象中获取的属性值,这种方式比较适合于大数据量,第一种方式是先获取key值,然后通过key在哈希表中寻找value值,效率太低
-
哈希表/散列表数据结构
-
hashMap集合底层是哈希表/散列表数据结构
-
哈希表是一个怎样的数据结构?
- 哈希表是一个数组和单向链表的结合体,一维数组中每一个元素是一个单向链表
- 数组:在查询方面效率很高,随机增删方面效率很低
- 单向链表:在随机增删方面效率很高,查询方面效率很低
- 哈希表将以上两种数据结构融合在一起,充分发挥他们各自的优点
-
- HashMap集合底层源代码
- public class HashMap{
- //HashMap底层实际上是一个数组(一维数组)
- Node<K,V>[] table;
- //静态的内部类HashMap.Node
- static class Node<K,V>{
- final int hash;//哈希值是key的hashCode()方法的执行结果,hash值通过哈希函数/算法,可以计算出数组元素的下标
- final K key;
- V value;
- Node<K,V> next; //下一个节点的内存地址
- }
- }
- 最主要掌握的是:
- map.put(k,v);
- v=map.get(k);
- HashMap集合的key部分特点:
- 无序,不可重复
- 为什么无序?因为不一定挂到哪个单向链表上
- 不可重复是怎么保证的?equals方法来保证HashMap集合的key不可重复,key重复的话value会覆盖
- 放在HashMap集合key部分的元素其实就是放到了HashSet集合中
- 所以HashSet集合中的元素也需要同时重写hashCode()+equals()方法
- 哈希表HashMap使用不当无法发挥性能!
- 假设将所有的hashCode()方法返回值固定为某个值,那么会导致哈希表底层变成了单向链表,这种情况我们称为散列分布不均匀
- 什么是散列分布均匀呢?
- 假设有100个元素,10个单向链表,那么每一个单向链表有10个节点,这是最好的是散列分布均匀的
- 假设将所有的hashCode()方法返回值都定为不一样的值可以吗,有什么问题?
- 不行,因为这样的话导致底层哈希表就成为一维数组了,没有链表的概念,也属于散列分布不均匀
- 散列分布均匀需要你重写hashCode方法时有一定的技巧
-
HashMap集合的默认初始化容量是16,默认加载因子是0.75
- 这个默认加载因子是当HashMap集合底层数组的容量达到75%的时候,数组开始扩容,扩容之后的容量是原来的2倍
- 重点:记住HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,这是因为达到散列均匀,为了提高HashMap集合的存取效率
- 两个对象的equals相同,两个对象的hashCode必须相同,两个对象的equals不同,两个对象的hashCode可以相同,也可以不同
-
向Map中集合中存,以及从Map集合中取,都是先调用hashCode方法,然后再调用equals方法,equals方法可调用,也可能不调用
- 拿put(k,v)举例,什么时候equals不会调用?
- k.hashCode()方法返回哈希值
- 哈希值经过哈希算法转换成数组下标
- 数组下标位置上如果是null,equals不需要执行
- 拿get(k)举例,什么时候equals不会调用?
- k.hashCode()方法返回哈希值
- 哈希值经过哈希算法转换成数组下标
- 数组下标位置上如果是null,equals不需要执行
- 在重写了equals方法之后,equals方法返回值为true的,hash值按规定也必须相同,因为以前hashCode返回的是对象内存地址,所以应该重写
-
Student类重写了hashCode方法之后
-
在JDK8之后,如果哈希表的单向链表中元素超过8个,单向链表这种数据结构变成红黑树数据结构,当红黑树上的节点数量小于6时,会重新把红黑树变成单向链表,这种方式也是为了提高检索效率,二叉树的检索会再次缩小检索范围,提高效率
- 拿put(k,v)举例,什么时候equals不会调用?
-
HashMap和HashTable的K,V是否可以为null
-
HashMap:
-
可以看到,在key==null的时候,将key的hash值置为0,从而解决了当key为null时,走hashCode方法导致空指针异常,value也可以为null
- HashTable
-
总结:
- HashMap的key和value都可以为null
- HashTable的key和value都不能为null
- HashMap和HashTable底层都是哈希表的数据结构
- HashTable的初始化容量是11,默认加载因子是0.75,扩容是:(原容量*2)+1
- Properties
- 目前只需要掌握Properties属性类的相关方法即可
- Properties是一个Map集合,继承HashTable,properties的key和value都是String类型的
- Properties被称为属性类
- Properties是线程安全的
-
TreeSet集合
- TreeSet集合底层实际上是一个TreeMap(TreeSet构造方法会new一个TreeMap对象,增加元素底层是将元素加到TreeMap的key部分了)
- TreeMap底层是一个二叉树
- 放到TreeSet集合中的元素,等同于放到了TreeMap集合key部分了
- TreeSet集合中的元素:无序,不可重复,但是可以按照元素的大小顺序自动排序,称为:可排序集合
-
对自定义的类型来说,TreeSet可以排序吗
-
对于以下Person类型来说,无法排序,因为没有指定Person对象之间的比较规则,谁大谁小没有说明
-
出现这个异常原因是,Person类没有实现java.lang.Comparable接口
-
底层源代码
-
TreeSet集合可排序的方式有两种:
-
TreeSet集合可排序的第一种方式:实现Comparable接口
-
先按年龄升序,如果年龄一样再按照姓名升序
-
TreeSet集合中元素可排序的第二种方式:使用比较器的方式
-
最终结论:放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:
-
第一种:放在集合中的元素实现java.lang.comparable接口
- 第二种:在构造TreeSet或着TreeMap集合的时候给他传一个比较器进去
-
- Comparable和Comparator怎么选择呢?
- 当比较规则不会发生改变的时候,或者说比较规则只有1个的时候,建议实现Comparable接口
- 如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口
- TreeSet和HashMap的key都不能为null,HashSet的key可以为null
-
-
- Collections工具类
- java.util.Collection集合接口
- java.util.Collections集合工具类,方便集合的操作
- 对于自定义的类型进行排序时:
-
第一种方法:实现Comparable接口
-
第二种方法:使用比较器方式
-
Set集合怎么实现排序
-
-
集合总结:
-
List集合:
-
Set集合
-
Set集合因为没有下标,所以没有get方法,get方法只能在List集合中使用
-
-
- List集合特有方法:
-
IO流
-
什么是IO?
-
I:Input
-
O:Output
- 通过IO可以完成硬盘文件的读和写
-
-
IO流的分类(有多种分类方式):
-
一种方式是按照流的方向进行分类
-
以内存为参照物
-
往内存中去叫做输入(Input),或者叫做读(Read)
-
从内存中出来,叫做输出(Ouput),或者叫做写(Write)
-
-
- 另一种是按照读取数据方式不同进行分类:
- 有的流是按照字节的方式读取数据,一次读取一个字节byte,等同于一次读取8个二进制位(这种流是万能的),什么类型的文件都可以读取,包括:文本文件、图片、声音文件、视频文件...........
- a中国bc张三fe
- 第一次读:一个字节,正好读到'a'
- 第二次读:一个字节,正好读到'中'字符的一半
- 第三次读:一个字节,正好读到'中'字符的另一半
- 有的流是按照字符的方式读取数据,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,这种流不能读取:图片、声音、视频等文件,只能读取纯文本文件,连word文件也不能
- 假设文件file.java,采用字符流的话这样读:
- a中国bc张三fe
- 第一次读:'a'字符('a'字符在windows系统中占用1个字节)('a'在windows中占用一个字节,'a'在Java中占两个字节)
- 第二次读:'中'字符('中'字符在windows系统中占用2个字节)
- 假设文件file.java,采用字符流的话这样读:
- 有的流是按照字节的方式读取数据,一次读取一个字节byte,等同于一次读取8个二进制位(这种流是万能的),什么类型的文件都可以读取,包括:文本文件、图片、声音文件、视频文件...........
- 综上所述:
- 流的分类:
- 输入流、输出流
- 字节流、字符流
- 流的分类:
-
- java IO流有四大家族:
- java.io.InputStream 字节输入流
- java.io.OutputStream 字节输出流
- java.io.Reader 字符输入流
- java.io.Writer 字符输出流
- 四大家族的首领都是抽象类(abstract class)
- 所有的流都实现了:java.io.Closeable接口,都是可关闭的,都有close()方法,流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费很多资源,流毕竟是一个管道,是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费很多资源,养成好习惯,用完流一定要关闭
- 所有的输出流都实现了java.io.Flushable接口,都是可刷新的,都有flush方法
- 养成一个好习惯,输出流在最终输出之后,一定要记得flush刷新一下,这个刷新表示将管道中剩余未输出的数据强行输出完,刷新的作用就是清空管道
- 如果没有flush可能会导致丢失数据
- 注意:java中只要类名以”Stream“结尾的都是字节流,以”Reader/Writer“结尾的都是字符流
- java.io包下需要掌握的流有16个:
- 文件专属:
- java.io.FileInputStream
- java.io.FileOutputStream
- java.io.FileReader
- java.io.FileWriter
- 转换流:(将字节流转换成字符流)
- java.io.InputStreamReader
- java.io.OutputStreamWriter
- 缓冲流专属
- java.io.BufferedInputStream
- java.io.BufferedOutputStream
- java.io.BufferedReader
- java.io.BufferedWriter
- 数据流专属
- java.io.DataInputStream
- java.io.DataOutputStream
- 标准输出流
- java.io.PrintWriter
- java.io.PrintStream
- 对象流专属
- java.io.ObjectInputStream
- java.io.ObjectOutputStream
- 文件专属:
- java.io.FileInputStream进行文件读取(从硬盘到内存)
-
-
对程序进行改进
-
分析这个程序的缺点:一次读取一个字节byte,这样内存和硬盘交互的太频繁了,基本上时间/资源都耗费在交互上面了,能不能一次读取多个字节呢?可以
-
再次改进:
- 最终版:
-
FileInputStream的其他常用方法
- public int available() throws IOException:返回流当中剩余的没有读到的字节数量
- public long skip(long n) throws IOException:跳过几个字节不读
- public int available() throws IOException:返回流当中剩余的没有读到的字节数量
-
FileOutputSteam文件字节输出流,负责写(从内存到硬盘)
-
- 如果文件还没创建,路径中不能有还没创建的目录,因为他只能创建文件
-
FileOutputStream(File file, boolean append) append为true表示以追加的方式在原文件末尾写入,不会清空原文件
-
-
文件复制原理(一边读,一边写)但现在目录不能拷贝,后面学
-
-
FileReader文件字符输入流,只能读取普通文本 ,读取文本内容时,比较方便,快捷
-
FileWriter文件字符输出流,负责写的,只能输出普通文本
-
复制文本文件:使用FileReader和FileWriter进行拷贝的话,只能拷贝“普通文件”,java文件也是普通文件,能用记事本编辑的就是普通文件。和后缀没关系
- BufferedReader带有缓冲区的字符输入流
- 使用带有缓冲区的流不需要自定义char数组或者自定义byte数组,自带缓冲
- 当一个流的构造方法需要一个流的时候,这个被传近进来的流叫做“节点流”
- 外部负责包装的这个流叫做包装流\处理流
-
对于包装流来说,关闭的时候只需要关闭外层就行,里面的节点流会自动关闭
-
代码1:
-
转换流BufferedReader 以及缓冲流InputStreamReader:
-
转换流BufferedReader
-
数据专属流:DataOutputStream和DataInputStream:
-
标准输出流:java.io.PrintWriter和ava.io.PrintStream
-
File类
-
目录拷贝
-
序列化和反序列化
-
参与序列化和反序列化的对象必须实现Serializable接口
-
通过源代码发现,Serializable接口只是一个标志接口,这个接口当中什么代码都没有
- 那么他起到什么作用呢?
- 起到标识的作用,标志的作用,Java虚拟机看代这个类实现类这个接口,可能会对这个类进行特殊待遇,Serializable这个标志接口是给java虚拟机参考的,java虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号
- 序列化版本号有什么用呢
- 反序列化:
-
可以一次序列化多个对象吗?
- 可以,可以将对象放到集合当中,序列化集合
-
transient关键字修饰的变量表示游离的,不参与序列化操作!带有static关键字的变量也不能参与序列化
-
name不参与序列化
-
关于序列化版本号
-
如何自动生成序列化版本号?
- 选中类名 alt+回车
-
IO和Properties联合使用
-
IO流:文件的读和写
-
Properties:是一个Map集合,key和value都是String类型
-
-
-
多线程
-
多线程的静态方法,和对象是没有关系的,写在哪个栈里面,就哪个栈对象执行
-
什么是进程?什么是线程?
-
进程是一个应用程序(一个进程是一个软件)
-
线程是一个进程中的执行场景/执行单元
-
一个进程可以启动多个线程
-
-
对于Java程序来说,挡在DOS命令窗口中输入:
-
java HelloWorld 回程之后
-
会先启动JVM,而JVM就是一个进程(JVM本质上也是个软件)
-
JVM再启动一个主线程调用main方法
-
同时启动一个垃圾回收线程负责看护,回收垃圾
-
最起码现在的java程序中至少有两个线程并发
-
一个是执行main方法的主线程,一个是垃圾回收线程
-
-
进程和线程的关系?
-
一个进程可以有多个线程,但至少有一个线程;而一个线程只能在一个进程的地址空间内活动
-
资源分配给进程,同一个进程的所有线程共享该进程所有资源。
-
-
注意:
-
进程A和进程B的内存独立不共享
- 魔兽游戏是一个进程
- 酷狗音乐是一个进程
- 这两个进程是独立的,不共享资源
-
线程A和线程B呢?
-
在Java语言中,堆内存和方法区内存共享
-
但是栈内存独立,一个线程一个栈
- 假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发
-
-
- Java中之所以有多线程,目的就是为了提高程序的处理效率
- 比如main方法一个线程,垃圾回收一个线程,在main方法线程执行的时候,有垃圾时就可以启动垃圾回收器线程回收垃圾,main方法可以继续往下执行,谁执行完成谁就结束,两者互不干扰,效率提高
- 思考一个问题:
- 使用了多线程机制之后,main方法结束,是不是程序也可能不会结束,main发结束只是主线程结束了,其他的栈(线程)可能还在压栈弹栈
- 分析一个问题,对于单核的CPU来说,真的可以做到真正的多线程并发吗?
- 对于多核CPU电脑来说,真正的多线程并发是没有问题的
- 4核的cpu电脑来说,真正的多线程并发是没有问题的
- 什么是真正的多线程并发?
- t1线程执行t1的
- t2线程执行t2的
- t1不会影响t2,t2也不会影响t1,这叫做真正的多线程
- 单核CPU表示只有一个大脑
- 不能够做到真正的多线程并发,但是可以给人一种多线程并发的感觉,对于单核的CPU来说,在某一个时间点上实际上只能处理一件事,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人的感觉是,多个事情同时来做!!!
- 线程A:播放音乐
- 线程B:运行魔兽游戏
- 线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在执行,给我们的感觉是同时并发的
- 不能够做到真正的多线程并发,但是可以给人一种多线程并发的感觉,对于单核的CPU来说,在某一个时间点上实际上只能处理一件事,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人的感觉是,多个事情同时来做!!!
- 对于多核CPU电脑来说,真正的多线程并发是没有问题的
- 分析以下程序有几个线程(除了垃圾回收线程之外):
-
Java语言中,实现线程有两种方式,哪两种方式呢?
-
java支持多线程机制,并且Java已经将多线程实现了,我们只需要继承就行了
-
第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法
-
亘古不变的道理:方法体中的代码永远都是按照自上而下的顺序依次执行的,在执行myThread.start();只是为了开辟一个新的栈空间,只要新的栈空间开辟出来了,start方法就结束了,分支线程会自动调用run方法执行,主线程也会继续往下执行
-
以下程序的输出结果:有先有后,有多有少 这是怎么回事?
-
第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法
-
注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活
-
用匿名内部类:
-
-
-
线程生命周期:
-
JVM线程调度:
-
线程调度是指系统为线程分配处理器使用权的过程
-
-
怎么获取当前线程对象:
-
线程的sleep方法(进入阻塞状态)
-
关于Thread.sleep方法的一个面试题
-
slee睡眠太久了,如果希望半道上醒来,你应该怎么办?也就是说怎么叫醒一个正在睡眠的线程?
-
注意这个不是中断线程的执行,是中断线程的睡眠
-
-
在Java中怎么强行终止一个线程
-
这种方式存在很大的缺点,容易丢失数据,因为这种方式是直接将线程杀死了
-
线程没有保存的数据将会丢失,不建议使用
-
-
怎么合理的终止一个线程的执行,这种方式很常用
-
(这部分内容属于了解)关于线程调度
-
常见的线程调度模型有哪些
-
抢占式调度模型:优先让优先级高的线程使用,如果优先级一样,则随机调度
-
分时调度模型 :所有线程平均分配CPU使用时间
-
有一些编程语言,线程调度模型采用这种方式
-
-
-
Java中提供了哪些方法是和线程调度有关系的呢
- 实例方法:
-
void setPriority(int new Priority) 设置线程的优先级
-
int getPriority() 获取线程优先级
-
最低优先级是1,默认优先级是5,最高优先级是10
- 优先级越高,(大概率)越大的可能抢到CPU,是个大概率事件
-
- void join() 合并线程
- class MyThread1 extends Thread{
- public void doSome(){
- MyThread2 t=new MyThread2();
- t.join();//当前线程进入阻塞状态t线程执行,直到t线程执行结束,当前线程才可以执行
- }
- }
- class MyThread2 extends Thread{
- }
- 这里并不是说真的合并成一个栈,另一个栈不在了,而是栈之间协调了
- class MyThread1 extends Thread{
-
-
静态方法:
-
static void yield() 让位方法
-
暂停当前正在执行的线程对象,并执行其他线程
-
yield方法不是阻塞方法,让当前线程让位,让给其他线程
-
yield方法的执行会让当前线程从“运行状态”回到“就绪状态”(sleep方法是从运行状态回到阻塞状态)
-
虽然让了,但是也不一定能抢到
-
-
- 实例方法:
-
-
关于多线程环境下,数据安全问题
-
为什么这个是重点:
- 以后再开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写
- 做重要的是,你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要的是关注这些数据,在多线程并发环境下是否是安全的(重点!!!)
- 什么时候数据在多线程并发的环境下会存在安全问题呢?
- 条件一:多线程并发
- 条件二:有共享数据
- 条件三:共享数据有修改的行为
- 怎么解决线程安全问题呢?
- 当多线程并发环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全的问题,怎么解决这个问题呢?
- 线程排队执行(不能并发)
- 用排队机制解决线程安全问题
- 这种被称为“线程同步机制”
- 专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队
- 线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率,数据不安全,没有效率的事
- 说到线程同步这块,涉及到这两个专业术语:
- 同步编程模型
- 线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等待谁,这种编程模型叫做异步编程模型,对于单核CPU来说,其实就是多线程并发
- 异步编程模型
- 线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说t2线程执行的时候,必须等待t1线程的执行结束,两个线程之间发生了等待关系,这就是异步编程模型
- 效率较低,线程排队执行
- 异步就是并发,同步就是排队
- 同步编程模型
- 银行账户:
- 不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
- 使用线程同步解决线程安全问题:
-
当运行状态的线程遇到synchronized关键字代码块时候,会进入锁池lockpool,在锁池中找共享对象的对象锁,线程进入锁池找共享对象的对象锁的时候,会释放之前占有的CPU时间片,有可能找到,有可能没找到,没找到则在锁池中等待,如果找到了会进入就绪状态继续抢夺CPU时间片
-
把this换成actno也可以,因为actno是是实例变量,一个对象只有一份,只要是共享的对象就行
-
this 换成obj不可以,因为obj是局部变量,每次调用这个方法就会创建一个新对象
- 写成“abc”也可以,因为”abc“在字符串常量池中,地址固定,但不要这么写,因为这个字符串是所有线程对象都共享的,就会导致你去存款,其他所有人都要等你,不用这样,只需同一个账户对象之间发生共享就行
- Java中有三大变量?
- 实例变量:在堆中
- 静态变量:在方法区中
- 局部变量:在栈中
- 以上三大变量中:
- 局部变量永远不会存在线程安全问题
- 因为局部变量不共享(一个线程一个栈)
- 局部变量在栈中,所以局部变量永远不会共享
- 实例变量在堆中,堆只有一个
- 静态变量在方法区中,方法区只有一个
- 堆和方法区都是多线程共享的,所以可能存在线程安全问题
- 以上三大变量中:
- 扩大同步范围,效率会更低
- synchronized出现在实例方法上:
-
synchronized的三种写法
-
第一种:同步代码块
-
灵活
- synchronized(线程共享对象){
- 同步代码块
- }
-
- 第二种:在实例方法上使用synchronized
- 表示共享对象一定是this
- 并且同步代码块是整个方法体
- 第三种:在静态方法上使用synchronized关键字
- 表示找类锁
- 类锁永远只有1把
- 就算创建了100个对象,那么类锁也只有1把
-
- 当多线程并发环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全的问题,怎么解决这个问题呢?
- 死锁
-
聊一聊,我们以后开发中应该怎么解决线程安全问题
-
是一上来就选择线程同步吗?synchronized
-
不是 ,synchronized会让程序的效率降低,用户体验不好,系统的用户吞吐量降低,用户体验差,在不得已的情况下在选择线程同步机制
-
- 第一种方案:尽量使用局部变量代替实例变量和静态变量
- 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了,(1个线程一个对象,100个线程100个对象)
- 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了,线程同步机制
-
-
- 守护线程:
- java语言种线程分为两大类:
- 一类是:用户线程
- 一类是:守护线程(后台线程)
- 其中具有代表性的就是:垃圾回收线程(守护线程)
- 守护线程的特点:
- 一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束
- 注意:主线程main方法是一个用户线程,主线程不能设置为守护线程。因为一个线程可以在它运行之前被设置为守护进程,并且一旦程序启动主线程就开始运行,因此不能被设置为守护线程。
- 守护线程用在什么地方?
- 每天00:00的时候系统数据自动备份
- 这个需要使用定时器,并且可以将定时器设置为守护线程,一直在那里看着,每到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没必要进行数据备份了
- java语言种线程分为两大类:
-
定时器
-
定时器的作用:
- 间隔特定的时间,执行特定的程序
- 每周都要进行银行账户的总账操作
- 每天要进行数据的备份操作
- 间隔特定的时间,执行特定的程序
- 在实际的开发中,每隔多久就执行一段特定的程序, 这种需求是很常见的,那么在Java中其实其实可以采用多种方式实现
- 可以使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务,这种方式是最原始的定时器(比较low)
- 在Java的类库中已经写好了一个定时器,java.util.Timer,可以直接拿来使用,不过这种方式在目前的开发中也很少使用,因为现在很多高级的框架都支持定时任务
- 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器任务
- java.util.Timer
- 构造方法:Timer(){}
- void schedule(TimerTask task,Date firstTime,long period)
- 安排指定的任务在指定的时间开始开始,每隔多久执行一次
- TimerTask实现了Runnable
- 创建Timer的对象时候会自动开启一个Timer-0线程,调用的schedule方法会在Timer对象创建的线程中执行
-
-
实现线程的第三种方式:实现Callable接口(JDK新特性)
-
这种方式实现的线程可以获取线程的返回值
- 之前讲解的那两种方式是无法获取线程的返回值的,因为run方法返回void
- 思考:
- 系统委派一个线程去执行一个任务,该线程执行完任务,可能会有一个执行结果,我们怎么能拿到这个执行结果呢?
- 使用第三种方式:实现Callable接口方式
- 系统委派一个线程去执行一个任务,该线程执行完任务,可能会有一个执行结果,我们怎么能拿到这个执行结果呢?
- 这种方式的优点是:可以获取到线程的执行结果
- 这种方式的缺点是:效率较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低
-
-
关于Object类中的wait和notify方法(生产者和消费者模式)
-
wait和notify方法不是线程对象的方法,是Java中任何一个Java对象都有的方法,因为这两个方式是Object类中自带的,wait方法和notify方法不是通过线程对象调用,不是这样的,t.wait(),也不是这样的t .notify()...不对
- wait()方法作用?
- Object o=new Object()
- o.wait();
- 表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止,
- o.wait方法的调用,会让当前线程(正在o对象上活动的线程)进入等到状态
- notify()方法作用
- Object o=new Object();
- o.notify();
- 表示:唤醒正在o对象上等待的线程
- 还有一个notifyAll方法:这个方法是唤醒o对象上处于等待的所有线程
-
-
wait方法和notify方法是建立在synchronized线程同步基础之上的,因为多线程要同时操作一个仓库,有线程安全问题
-
-
-
-
反射机制
-
反射机制有什么作用?
-
通过java语言中的反射机制可以操作字节码文件
-
优点类似于黑客(可以读和修改字节码文件)
-
-
反射机制的相关类在哪个包下?
-
java.lang.reflect.*;
-
-
反射机制相关的类有哪些?
-
java.lang.Class:代表整个字节码,代表一个类型,代表整个类
- java.lang.reflect.Method:代表字节码中的方法字节码
- java.lang.reflect.Constructor:代表字节码中的构造方法字节码
- java.lang.reflect.Field:代表字节码中的属性字节码(静态变量+实例变量)
-
- 代码
- //java.lang.class
- public class User{
- //Field
- int no;
- //Constructor
- public User(){
- }
- public User(int no){
- this.no=no;
- }
- //Method
- public void setNo(int no){
- this.no=no;
- }
- public int getNo(){
- return no;
- }
- 获取字节码的三种方式:
-
获取到Class,能干什么?
-
验证反射机制的灵活性:
-
java代码写一遍,在不改变源代码的基础之上,可以做到不同对象的实例化,非常之灵活,符合OCP的开闭原则,对扩展开放,就修改关闭
-
后期要学习框架,而在工作中,也都是使用高级框架:
-
包括:Spring、SpringMVC、MyBatis、Spring 、Struts、Hibernate
- 这些高级框架底层实现原理,都采用了反射机制,所以反射机制还是很重要的,学会了反射机制有利于你理解刨析框架底层源代码
-
- 研究一下:Class.forName()发生了什么?
-
研究一下文件路径问题:
-
怎么获取一个文件的绝对路径,以下讲解的这种方式是通用的,但前提是:文件需要在类路径下,才能用这种方式
-
- 以流的形式直接返回
-
资源绑定器:
-
关于JDK中自带的类加载器
-
什么是类加载器ClassLoader?
-
专门负责类加载的命令/工具
-
- JDK中自带了3个类加载器
- 启动类加载器
- 扩展类加载器
- 应用类加载器
- 假设有这样一段代码:
- String s="abc";
- 代码在开始执行之前,会将所需要的类加载到JVM当中,通过类加载器加载,看到以上代码,类加载器会找String.class文件,找到就加载,那么是怎么进行加载的呢?
- 首先通过“启动类加载器”加载
- 注意:启动类加载器专门加载:D:\app\java\jre\lib\rt.jar
- rt.jar中都是JDK最核心的类库
- 如果通过“启动类加载器”加载不到,会通过“扩展类加载器”加载
- 注意:扩展类加载器专门加载:D:\app\java\jre\lib\ext\*.jar
- 如果“扩展类加载器”没加载到,会通过“应用类加载器”加载
- 注意:应用类加载器专门加载:classpath路径下的jar包/class文件
- 首先通过“启动类加载器”加载
- 双亲委派机制:
- Java为了保证程序安全,防止核心API被随意篡改,使用了双亲委派机制
- 优先从“启动类加载器”中加载,这个称为“父”,“父”无法加载到,再从扩展类加载器中加载,这个称为“母”,双亲委派。如果都加载不到,才会考虑从"应用类加载器中国加载”,直到加载到为止
-
- 获取Field
-
反编译Field
-
通过反射机制访问对象的属性
-
可变长度参数
-
反射Method
-
反编译方法
-
通过反射机制怎么调用一个对象的方法
-
反编译Constructor
-
反射机制调用构造方法
-
获取父类和父接口:
-
-
注解annotation
-
注解,或者叫做注释类型,英文单词是:Annotation
-
注解Annotation是一种引用数据类型,编译之后也是生成xxx.class文件
-
怎么自定义注解呢?语法格式?
-
[修饰符列表] @interface 注解类型名{
-
}
-
-
注解怎么使用,用在什么地方?
-
第一:注解使用时的语法格式是:
-
@注解类型名
-
-
第二:注解可以出现在类上、接口上、枚举上、属性上、方法上等.......
-
注解还可以出现在注解上
-
-
-
JDK内置了哪些注解
-
java.lang包下的注释类型:
-
掌握
-
用@Deprecated注释的程序元素
-
不鼓励程序员使用这样的元素,通常是因为他很危险或存在更好的选择
-
-
掌握
-
Override表示一个方法声明打算重写超类中的另一个方法声明
-
-
不用掌握:
-
SuppreWarnings指示应该在注释元素(以及包含该元素中的所有程序元素)中取消显示指定的编译器警告
-
-
-
-
什么是元注解?
-
只能用来标注”注解类型“的”注解“,称为元注解
-
常见的元注解有哪些?
-
Target
-
Retention
-
-
关于Target注解:
-
这是一个元注解,用来标注“注解类型”的注解
-
这个Target注解用来标注“被标注的注解”可以出现在哪些位置上
-
@Target(ElementType.METHOD) :表示“被标注的注解”只能出现在方法上
@Target(value={ElementType.CONSTRUCTOR,ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElemetType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE})
- 表示该注解可以出现在:构造方法上、成员变量上(字段上)、局部变量上、方法上等等
-
-
关于Retention注解:
-
这是一个元注解,用来标注“注解类型”的注解
- 这个Retention注解用来表示“被标注的注解”最终保存在哪里
- @Retention(RetentionPolicy.SOURCE):表示该注解只被保存在java源文件中
- @Retention(RetentionPolicy.CLASSS):表示该注解被保存在class字节码文件中
- @Retention(RetentionPolicy.RUNTIME):表示该注解被保存在class文件中,并且可以被反射机制所读取
-
-
- 注解中定义属性:
-
如果一个注解的属性名是value时,属性名可以省略
-
注解的属性除了value还有其他值的话,就不能省略value了
-
注解的属性类型可以是什么
-
反射注解:
-
代码:
-
如果@Retention(RetentionPolicy.RUNTIME)不是RUNTIME,反射机制就不能读取到该注解
-
通过反射注解获取注解对象的属性值
-
注解在开发中有什么作用
-
需求:假设有这样一个注解,叫做:@Id,这个注解只能出现在类上面,当类上有这个注解的时候,要求这个类中必须有一个int类型的属性,如果没有这个属性,就报异常,如果有这个属性,则正常执行
-
如果类中没有id属性:
-
-
-