1.Java基本功
1.1Java入门(基础概念与常识)
1.1.1Java语言有哪些特点?
1. 简单易学;
2. 面向对象(封装,继承,多态);
3. 平台无关性(Java虚拟机实现平台无关性);
4. 可靠性;
5. 安全性;
6. 支持多线程(C++语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而Java语言却提供了多线程支持);
7. 支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的,因此Java语言不仅支持网络编程并且很方便);
8. 编译与解释并存;
1.1.2关于JVM、JDK和JRE最详细通俗的解答
- JVM:java虚拟机,是运行Java字节码的虚拟机。JVM有针对不同的操作系统的特定实现,这也是Java能够跨平台的原因。字节码和不同系统的JVM实现是Java语言“一次编译,随处可以运行的关键所在”。
- JDK:Java Development Kit,它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。
- JRE:Java运行环境。它是运行已编译Java程序所需的所有内容的集合,包括JVM,Java类库,Java命令和其他的一些基础构件。但是,它不能用于创建新程序。
1.1.3Oracle JDK和OpenJDK的对比
1.1.4Java和C++的区别
- 都是面向对象的语言,都支持封装、继承、多态
- Java不提供指针来直接访问内存,程序内存更加安全
- Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多实现。
- Java自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
- 在C语言中,字符串或字符数组最后都会有一个额外的字符’\0’来结束。但是,Java语言没有结束符这一概念。
1.1.5什么是Java程序的主类,应用程序和小程序的主类有何不同?
一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中年,这个主类是指包含main()方法的类。而在Java小程序中,这个主类是一个集成自系统类JApplet或Applet的子类。应用程序的主类不一定要求是public 类,但小程序的主类要求必须是public类。主类是Java程序执行的入口点。
1.1.6Java应用程序与小程序有哪些差别?
简单说,应用程序是从主线程启动(也就是main()方法)。applet小程序没有main()方法,主要是嵌在浏览器页面上运行(调用init()或者run()来启动),嵌入浏览器这点跟flash的小游戏类似。
1.1.7import java和javax有什么区别?
刚开始的时候JavaAPI所必须的包是java开头的包,javax当时只是扩展API包来使用。然而随着时间的推移,javax逐渐扩展称为Java API的组成部分。但是,将扩展从javax移动到java包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定javax包将称为标准API的一部分。所以,实际上java和javax没有区别。这都是一个名字。
1.1.8为什么说Java语言“编译与解释并存”’?
高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性编译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著(也就是源码),都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。
Java语言既具有编译型语言的特征,也具有解释型语言的特征,因为Java语言要先编译,后解释两个步骤,有Java编写的程序需要先经过编译步骤,生成字节码(*.class文件),这种字节码必须由Java解释器来解释执行。因此,我们可以认为Java语言编译与解释并存。
1.2Java语法
1.2.1字符型常量和字符串常量的区别
-
形式上:字符常量是单引号引起的一个字符;字符串常量是双引号引起的0个或若干个字符
-
含义上:字符常量相当于一个整形值(ASCII),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置)
-
占内存大小:字符常量只占两个字节;字符串常量占若干个字节(注意:char在Java中占两个字节),字符封装类Character有一个成员常量Character.SIZE值为16,单位是bits,该值除以8(1byte=8bits)后就可以得到2个字节
Java中基本类型所占字节大小如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QGphNEty-1600397805156)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200917145035357.png)]
1.2.2关于注释?
Java中的注释有三种:
- 单行注释
- 多行注释
- 文档注释
在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释),使我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。
1.2.3标识符和关键字的区别是什么?
在我们编写程序的时候,需要大量地为程序,类,变量,方法等取名字,于是就有了标识符,简单来说,标识符就是一个名字。但是有一些标识符,Java语言已经赋予了其特殊的含义,只能用语言特定的地方,这种特殊的标识符就是关键字。因此,关键字是被赋予特殊含义的标识符。比如,在我们日常生活中,“警察局"这个名字已经被赋予了特殊的含义,所以如果你开一家店,店的名字不能叫"警察局","警察局"就是我们日常生活中的关键字。
1.2.4Java中有哪些常见的关键字?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jH0DE2cG-1600397805161)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200917150539167.png)]
1.2.5自增自减运算符
在写代码的过程中,常见的一种情况是需要某个整数的类型变量增加1或减少1,Java提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(–)。
++和–运算符可以放在变量之前,也可以放在变量之后,当运算符放在变量之前是(前缀),先自增/减,在赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。例如,当b = ++a是,先自增(自己增加1),在赋值(赋值给b);当b = a++时,先赋值(赋值给b),再自增(自己增加1)。也就是,++a输出的a+1的值,a++输出额是a值。用一句口诀就是:“符号在前就先加/减,符号在后就后加/减"。
1.2.6continue、break和return的区别是什么?
在循环结构中,当循环条件不满足或者循环次数达到要求是,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后,提前终止循环,这就需要用到下面几个关键字:
- continue:指跳出当前的这一次循环,继续下一次循环。
- break:指跳出整个循环体,继续执行循环下面的语句。
return 用于跳出所在的方法,结束该方法的运行。return一般有两种用法:
- return; :直接使用return 结束方法执行,用于没有返回值函数的方法
- return value; :return一个特定值,用于有返回值函数的方法
1.2.7Java泛型了解么?什么是类型擦除?介绍一下常用的通配符?
Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许程序员在编译时监测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
Java的泛型是为泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦掉
List<Integer> list = new ArrayList<>();
list.add(12);
//这里直接添加会报错
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通过反射添加,是可以的
add.invoke(list, "kl");
System.out.println(list)
泛型一般有三种使用方式泛型类、泛型接口、泛型方法。
-
泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 //在实例化泛型类时,必须指定T的具体类型 public class Generic<T>{ private T key; public Generic(T key) { this.key = key; } public T getKey(){ return key; } }
如何实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
-
泛型接口:
public interface Generator<T> { public T method(); }
实例泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{ @Override public T method() { return null; } }
实现泛型接口,指定类型:
class GeneratorImpl<T> implements Generator<String>{ @Override public String method() { return "hello"; } }
-
泛型方法:
public static < E > void printArray( E[] inputArray ) { for ( E element : inputArray ){ System.out.printf( "%s ", element ); } System.out.println(); }
使用:
// 创建不同类型数组: Integer, Double 和 Character Integer[] intArray = { 1, 2, 3 }; String[] stringArray = { "Hello", "World" }; printArray( intArray ); printArray( stringArray );
常用的通配符为:T,E,K,V,?
- ?表示不确定的java类型
- T(type)表示具体的一个java类型
- K V(key value)分别代表java键值中的key value
- E (element)代表Element
1.2.8==和equals的区别
:他的作用是判断两个对象的地址是不是相等。即判断连个对象是不是同一个对象。(基本数据类型比较的是值,引用数据类型==比较的是内存地址)
因为Java只有值传递,所以,对于==来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是应用变量存的值是对象的地址。
equals():它的作用也是判断两个对象是否相等,它不能用于比较基本数据类型的变量。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。
Object类equals()方法:
public boolean equals(Object obj) {
return (this == obj);
}
equals()方法存在两种使用情况:
-
情况1:类没有覆盖equals()方法。则通过equals()比较该类的连个对象时,等价于通过"=="比较这连个对象。使用的默认是Object类equals()方法。
-
情况2:类覆盖了equals()方法,一般,我们都覆盖equals()方法来比较两个对象的内容是否相等,若它们的内容相等,则返回true(即,认为这两个对象相等)。
举个例子:
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
说明:
- String中的equals()方法是被重写过的,因为Object的equals()方法是比较的对象的内存地址,而String的equals()方法比较的是对象的值。
- 当创建String类型的对象是,虚拟机会在常量池中查看有没有存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
String 类equals()方法:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
1.2.9hashCode()与equals()
面试官可能会问你:“你重写过hashcode()和equals()么,为什么重写equals()时必须重写hashCode()方法?
1)hashCode的介绍:
hashCode()的作用就是获取哈希码,也称散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object类中,这就意味着Java中的任何类都包含有hashCode()函数。另外需要注意的是:Object 的hashcode方法是本地方法,也就是用c语言或c++实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键"快速的检索出对应的”值"。这其中就利用到了散列码!(可以快速找到所需要的对象)
2)位什么要有hashCode?
我们以"HashSet如何检查重复"为例子来说明为什么要有hashCode?
当你把对象加入HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashcode值做比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashCode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会散列到其他位置。这样我们就大大减少了equals的次数,相应就大大提高了执行速度。
3)位什么重写equals时必须重写hashCode方法?
如果两个对象相等,则hashcode一定是相同的。两个对象相等,对两个对象分别调用equals方法都是返回true.但是,两个对象有相同的hashcode值,他们也不一定是相等的。因此,equals方法被覆盖过,则hashCode方法也必须覆盖。
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
4)为什么两个对象有相同的hashcode值,他们也不一定是相等的?
因为hashCode()所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的hashcode值。
我们上面也提到了HashSet,如果HashSet在对比的时候,同样的hashcode有多个对象,它会使用equals()来判断是否真的相同。也就是说hashcode只是用来缩小查找成本。
1.3基本数据类型
1.3.1Java中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢?
Java中有8种基本数据类型,分别为:
- 6种数字类型:byte、short、int、long、float、double
- 1种字符类型:char
- 1种布尔型:boolean
这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean
基本类型 | 位数 | 字节 | 默认值 |
---|---|---|---|
byte | 8 | 1 | 0 |
short | 16 | 2 | 0 |
int | 32 | 4 | 0 |
long | 64 | 8 | 0L |
float | 32 | 4 | 0f |
double | 64 | 8 | 0d |
char | 16 | 2 | ‘u0000’ |
boolean | 1 | false |
对于boolean,官方文档未明确定义,它依赖于JVM厂商的具体实现。逻辑上理解是占用1位,但是实际中会考虑计算机高效存储因素。
注意:
- Java里使用long类型的数据一定要在数值后面加上L,否则将作为整形解析
- char a = ‘h’ char:单引号; String a = “hello” :双引号
1.3.2自动装箱与拆箱
-
装箱:将基本类型用他们对应的引用类型包装起来;
-
拆箱:将包装类型转换为基本数据类型;
更多内容见:深入剖析Java中的装箱和拆箱
1.3.3 8种基本类型的包装类和常量池
Java基本类型的包装类的大部分都实现了常量池技术,即:
Byte,Short,Integer,Long,Character,Boolean;前面4种包装类默认创建了[-128,127]的对应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean直接返回true or false.如果超出对应范围仍然会去创建新的对象。为啥把缓存设置为[-128,127]区间?
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}
两种浮点数类型的包装类Float,Double并没有实现常量池技术。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
Integer缓存源代码:
/**
*此方法将始终缓存-128 到 127(包括端点)范围内的值,并可以缓存此范围之外的其他值。
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
应用场景:
-
Integer i1 = 40;Java在编译的时候会直接将代码封装成Integer i1 = Integer.valueof(40);从而使用常量池中的对象。
-
Integer i1 = new Integer(40);这种情况下会创建新的对象。
Integer i1 = 40; Integer i2 = new Integer(40); System.out.println(i1 == i2);//输出 false
Integer比较更丰富的一个列子:
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("40=i5+i6 " + (40 == i5 + i6));
结果:
i1=i2 true
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
40=i5+i6 true
解释:
语句i4 == i5 + i6,因为+这个操作符不适用于Integer这个对象,首先i5 和 i6进行自动拆箱操作,进行数值相加,即i4 = = 40.然后Integer对象无法与数值进行直接比较,所以i4就自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较。
1.4方法(函数)
1.4.1什么是方法的返回值?返回值在类的方法里的作用是什么?
方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值得作用是接收出结果,使得它可以用于其他的操作!
1.4.2为什么Java中只有值传递?
首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接受的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。
Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值得一个拷贝,也就是说,方法不能修改传递给他的任何参数变量的内容。
下面通过三个例子来给大家说明
example1
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
结果:
a = 20
b = 10
num1 = 10
num2 = 20
解析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R0pQ2V4b-1600397805164)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200917175038060.png)]
在swap方法中,a、b的值进行交换,并不会影响到num1、num2。因为,a、b中的值,只是从num1、num2复制过来的。也就是说,a,b相当于num1、num2的副本,副本的内容无论怎么修改,都不会影响到原件本身。
通过上面的例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看example2.
example2
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}
public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}
结果:
1
0
解析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q06joi9z-1600397805167)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200917175829633.png)]
array被初始化arr的拷贝,也就是一个对象的引用,也就是说array和arr指向的是同一个数组对象。因此,外部对引用对象的改变会反映到所对应的对象上。
通过example2,我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
很多程序设计语言(特别是,C++和Pascal)提供了两种参数传递的方式:值传递和引用传递。有些程序员认为Java程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所有下面给出一个反例来详细地阐述一下这个问题。
example
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Student s1 = new Student("小张");
Student s2 = new Student("小李");
Test.swap(s1, s2);
System.out.println("s1:" + s1.getName());
System.out.println("s2:" + s2.getName());
}
public static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println("x:" + x.getName());
System.out.println("y:" + y.getName());
}
}
结果:
x:小李
y:小张
s1:小张
s2:小李
解析:
交换之前:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OETObQPZ-1600397805170)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200917180839973.png)]
交换之后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8OUAw0Ta-1600397805172)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200917180935932.png)]
通过上面两张图可以很清晰的看出:方法并没有改变存储在变量s1和s2中对象的引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这连个拷贝
总结
Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。
下面再总结一下Java中参数的使用情况:
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
1.4.3重载和重写的区别
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应是,你就要覆盖父类方法
1.4.3.1重载
发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
1.4.3.2重写
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
- 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法的访问修饰符为private/final/static 则子类就不能重写该方法,但是被static修饰的方法能够被再次声明。
- 构造方法无法被重写
综上:重写就是子类对弗雷方法的重新改造,外部样子不能改变,内部逻辑可以改变
重写和重载的区别:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LtoRmLgE-1600397805176)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200917200631494.png)]
方法的重写要遵循“两同两小”
- "两同"即方法名相同、形参列表相同;
- "两小"指的是子类方法返回值类型应比弗雷方法返回值类型更小或相等,子类方法声明抛出的异常应比父类方法抛出的异常类更小或相等;
- “一大”指的是子类方法的权限应比父类方法的访问权限更大或相等。
1.4.4深拷贝vs浅拷贝
-
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
-
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nVcJa1bS-1600397805180)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200917202035580.png)]
1.4.5方法的四种类型
-
五参数无返回值得方法
// 无参数无返回值的方法(如果方法没有返回值,不能不写,必须写void,表示没有返回值) public void f1() { System.out.println("无参数无返回值的方法"); }
-
有参数无返回值的方法
/** * 有参数无返回值的方法 * 参数列表由零组到多组“参数类型+形参名”组合而成,多组参数之间以英文逗号(,)隔开,形参类型和形参名之间以英文空格隔开 */ public void f2(int a, String b, int c) { System.out.println(a + "-->" + b + "-->" + c); }
-
有返回值无参数的方法
// 有返回值无参数的方法(返回值可以是任意的类型,在函数里面必须有return关键字返回对应的类型) public int f3() { System.out.println("有返回值无参数的方法"); return 2; }
-
有返回值有参数的方法
// 有返回值有参数的方法 public int f4(int a, int b) { return a * b; }
-
return 在无返回值方法的特殊使用
// return在无返回值方法的特殊使用 public void f5(int a) { if (a > 10) { return;//表示结束所在方法 (f5方法)的执行,下方的输出语句不会执行 } System.out.println(a); }
2.Java面向对象
2.1类和对象
2.1.1面向对象和面向过程的区别
- 面向过程:面向过程性能比面向对象高。因为累调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。
- 面向对象:面向对象易维护、易复用、易扩展。因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低。
面向过程:面向过程性能比面向对象高??
这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是因为他是面向对象语言,而是Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机器码。
而面向过程语言大多都是直接编译成机器码在电脑上执行,并且其他一些面向过程的脚本语言性能也并不一定比Java好。
2.1.2构造器Constructor是否可悲override?
Constructor不能被override(重写),但是可以overload(重载),所以你可以看到一个类中有多个构造函数的情况。
2.1.3在Java中定义一个不做事且没有参数的构造方法的作用
Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”(即父类的无参构造方法)。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
2.1.4成员变量与局部变量的区别有哪些?
- 从语法形式上看:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被public、private、static等修饰符所修饰,而局部变量不能被访问控制修饰符及static修饰;但是,成员变量和局部变量都能被final修饰。
- 从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
- 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
- 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被final修饰的成员变量必须显式地赋值),而局部变量则不会自动赋值。
2.1.5创建一个对象用什么运算符?对象实体与对象应用有何不同?
new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向他(可以用n条绳子系住一个气球)。
2.1.6一个类的构造方法的作用是什么?若一个类没有声明构造方法,该程序能正确执行吗?为什么?
主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java就不会再添加默认的无参构造方法了,这时候,就不能直接new一个对象而不传递参数了,所以我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们冲在了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。
2.1.7构造方法有哪些特性?
- 名字与类名相同
- 没有返回值,但不能用void声明构造函数。
- 生成类的对象是自动执行,无需调用。
2.1.8在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?
帮助子类做初始化工作。
2.1.9对象的相等与指向他们的引用相等,两者有什么不同?
对象的相等,比的是内存中存放的内容是否相等,而引用相等,比较的是它们指向的内存地址是否相等。
2.2面向对象三大特征
2.2.1封装
封装是指把一个对象的状态(也就是属性)隐藏在对象内部,不允许外部直接访问对象内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也就没有什么意义了。就好像如果我们没有空调遥控器,那么我们就无法操控空调制冷就,空调本身就没有意义了(当然现在还有很多其他方法,这里只是为了举例子)。
public class Student {
private int id;//id属性私有化
private String name;//name属性私有化
//获取id的方法
public int getId() {
return id;
}
//设置id的方法
public void setId(int id) {
this.id = id;
}
//获取name的方法
public String getName() {
return name;
}
//设置name的方法
public void setName(String name) {
this.name = name;
}
}
2.2.2继承
不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性的继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间,提高我们的开发效率。
关于继承如下3点请记住:
- 子类拥有弗雷对象所有的属性和方法(暴扣私有属性和私有方法),但是父类中的私有属性和方法,子类是无法访问的,只是拥有
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
2.2.3多态
多态,顾名思义,表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例。
多态的特点:
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 对象类型不可变,引用类型可变;
- 方法具有多态性,属性不具有多态性;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用“只在子类存在但在父类不存在的”的方法;
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
2.3修饰符
2.3.1 在一个静态方法内调用一个非静态成员为什么是违法的
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
2.3.2静态方法和实例方法有何不同
- 在外部调用静态方法是,可以使用“类名.方法名"的方式,也可以使用”对象名.方法名“的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
2.3.3常见关键字总结:staitic,final,this,super
2.4接口和抽象类
2.4.1接口和抽象类的区别是什么?
- 接口的方法默认是public,所有方法在接口中不能有实现(Java8开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
- 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。
- 一个类可以实现多个接口,但只能实现一个抽象类。接口本身可以通过extends关键字扩展多个接口。
- 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。
- 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
备注
在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现了两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。详情见:https://github.com/Snailclimb/JavaGuide/issues/146
JDK9的接口被允许定义私有方法。
总结一JDK7~JDK9 Java中接口概念的变化:
- 在JDK7或更早版本中,接口里面只能有常量和抽象方法。这些接口方法必须由选择实现接口的类实现。
- JDK8的时候接口可以有默认方法和静态方法功能。
- JDK9在接口中引入了私有方法和私有静态方法。
2.5其他重要知识点
2.5.1String、StringBuffer和StringBuilder的区别是什么?String 为什么是不可变的?
简单的来说:String类中使用final关键字修饰字符数组来保存字符串,private final char
value[],所以String对象是不可变的。
补充:
在JDK9之后,String类的实现改用byte数组存储字符串private final byte[] value;
而StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串char[] value,但是没有用fianl关键字修饰,所以这两种对象都是可变的。
StringBuilder与StringBuffer的构造方法都是调用父类构造方法,也就是AbstractStringBuilder实现的,大家可以自行查阅源码。
AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}}
线程安全性
String 中的对象是不可变得,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法家里同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下,StringBuilder相比使用StringBuffer仅能获得10%~15%左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据:适用String
- 单线程操作字符串缓冲区下操作大量数据:适用StringBuilder
- 多线程操作字符串缓冲区下操作大量数据:使用StringBuffer
2.5.2Object类的常见方法总结
Object类是一个特殊的类,是所有类的父类。它主要提供了以下11个方法:
public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作
2.5.3==与equals(重要)
:它的作用是判断连个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型==比较的内存地址)。
equals():它的作用也是判断两个对象是否相等。但是一般有两种使用情况:
- 情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较两个对象。
- 情况2:类覆盖了equals()方法,一般,我们都覆盖equals()方法来比较两个对象内容是否相等;若它们内容相等,则返回true(即,认为这两个对象相等)。
举个例子:
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
说明:
- String中的equals方法是被重写过的,因为Object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
- 当创建String类型的对象是,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中创建一个String对象。
2.5.4hashCode与equals(重要)
面试官可能会问你:“你重写过hashCode和equals么,为什么重写equals时必须重写hashCode方法?”
2.5.4.1hashCode()介绍
hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用就是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”‘快速检索出对应的"值"。这其中就用到了散列码!(可以快速找到所需要的对象)
2.5.4.2为什么要有hashCode
我们先以“HashSet如何检查重复"为例子来说明为什么要有hashCode:当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与该位置的其他已经加入的对象的hashCode进行比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值得对象,这时就会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的haul,就会重新散列到其他位置。这样我们就可以大大减少了equals的次数,相应就大大提高了执行速度。
通过我们可以看出:hashCode()的作用就是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()在散列表中才有用,在其他情况下没有。在散列表中hashCode()的作用是获取对象的散列码,进而确定该对象在散列表中位置。
2.5.4.3
hashCode()与equals()的相关规定
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个对象分别调用equals方法都返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
- 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖
2.5.5Java序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用transient关键字修饰。
transient关键字的作用是:阻止实例中那些用此关键字修饰的变量序列化;当对象被反序列化是,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。
2.5.6获取用键盘输入常用的两种方法
方法1:通过Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
方法2:通过BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
3.Java核心技术
3.1集合
3.1.1Collections工具类和Arrays工具类常见方法总结
3.2异常
3.2.1Java异常类层次结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NcZPHn6c-1600397805184)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200917235051060.png)]
在Java中,所有的异常都有一个共同的祖先java.lang包中的Throwable类。Throwable:有两个重要的子类:Exception(异常)和Error(错误),二者都是Java异常处理的重要子类,各自都包含大量的子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中叫严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时JVM(Java虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不在有继续执行操作所需的内存资源是,将出现OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机视图执行应用是,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为他们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception类有一个重要的子类RuntimeException。RuntimeException异常由Java虚拟机抛出。NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、ArithmeticException(算术异常—,一个整数除以0时,抛出该异常)和ArrayIndexOutOfBoundsException(下标越界异常)。
注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。
3.2.2Throwable类常用方法
- public String getMessage():返回异常发生时的简要描述
- public String toString():返回异常发生时的详细信息
- public String getLocalizedMessage():返回异常对象的本地化信息。使用Throwable的子类覆盖的资格方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同。
- public void printStackTrace():在控制台上打印Throwable对象封装的异常信息
3.2.3try-catch-finally
- try块:用于捕获异常。其后可接零个或多个catch快,如果没有catch块,则必须跟一个finally快。
- catch块:用于处理try捕获到的异常。
- finally块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句是,finally语句块将在方法返回之前被执行。
在以下4种特殊情况下,finally块不会被执行:
- 在finally语句块第一行发生了异常。因为在其他行,finally块还是会得到执行
- 在前面的代码中用了System.exit(int)已退出程序。exit是带参函数;若该语句在异常语句之后,finally会执行
- 程序所在的线程死亡
- 关闭CPU
注意:当try语句和finally语句中都有return语句是,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会覆盖原始的返回值。如下:
public class Test{
pbulc static int f(int value){
try{
return value * value;
}finally {
if(value === 2){
return 0;
}
}
}
}
如果调用f(2),返回值将是0,因为finally语句的返回值覆盖了try语句块的返回值。
3.2.4使用try-with-resources来替代try-catch-finally
- 使用范围(资源的定义):任何实现java.lang.AutoCloseable或者"java.io.Closeable"的对象
- 关闭资源和final的执行顺序:在try-with-resources语句中,任何catch或finally块在声明的资源关闭后运行
Java中类似于InputStream、OutputStream、Scanner、PrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求,如下:
//读取文本文件的内容
Scanner scanner = null;
try{
Scanner = new Scanner(new File("D://read.txt"));
while(scanner.hasNext()){
System.out.println(scanner.nextLine());
}
}catch(FileNotFoundException e){
e.printStackTrace();
}finally{
if(scanner != null){
scanner.close();
}
}
使用Java7之后的try-with-resource语句改造上面的代码:
try(Scanner scanner = new Scanner(new File("read.txt"))) {
while(scanner.hasNext()){
System.out.println(scanner.nextLine());
}
}catch(FileNotFoundException fnfe){
fnfe.printStackTrace();
}
当然多个资源需要关闭的时候,使用try-with-resources实现起来也非常简单,如果你还是用try-catch-finally可能会带来很多问题。
通过使用分号分隔,可以在try-with-resources块中声明多个资源。
try(BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("text.txt"));BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt"))){
int b;
while((b = bin.read()) != -1){
bout.writee(b);
}
}catch (IOException e){
e.printStackTrace();
}
3.3多线程
3.3.1简述线程、程序、进程的基本概念。以及他们之间关系是什么?
线程与进程相似,但线程是一个比进程更小的执行单元。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作是,负担要比进程小的都,也正因为如此,线程也被称为轻量级进程。
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,他在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。线程是进程划分的更小的运行单位。线程和进程最大的不同在于基本上个进程是独立的而个线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内同时执行一个以上的程序段。
3.3.2线程有哪些基本状态?
Java线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jrvSrQEf-1600397805186)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200918093729951.png)]
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java线程状态变迁如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FKxxt8Qa-1600397805191)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200918094122981.png)]
由上图可以看出:
线程创建之后,它将处于NEW(新建)状态,调用start()方法后开始运行,线程这时候处于READY(可运行)状态。可运行状态的线程获得了CPU时间片(timeslice)后就处于RUNNING(运行)状态。
操作系统隐藏Java虚拟机(JVM)中的READY和RUNNING状态,它只能看到RUNNABLE状态,所以Java系统一般将这两个状态称为RUNNABLE(运行中)状态。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rqezGOsJ-1600397805192)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20200918094649886.png)]
当线程执行外套()方法之后,线程进入WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行呢状态,而TIME_WAITING(超时等待)状态相当于在等待状态的基础上增加了超时限制,比如通过sleep(long millis)方法或wait(long millis)方法可以将Java线程置于TIMED WAITING状态。当超时时间到达后Java线程将会返回到RUNNABLE状态。当线程调用同步方法是,在没有获取到锁的情况下,线程将会进入到BLOCKED(阻塞)状态。线程在执行Runnable的run()方法之后将会进入到TERMINATED(终止)状态。
3.4文件与I\O流
3.4.1Java中IO流分为几种?
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以划分为字节流和字符流;
- 按照流的角色划分为节点流和处理流
Java IO流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系,Java IO流的40多个类都是从如下4个抽象类基类中派生出来的。
- InputStream/Reader:所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I4y9ehE5-1600397805196)(https://camo.githubusercontent.com/639ec442b39898de071c3e4fd098215fb48f11e9/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d362f494f2d2545362539332538442545342542442539432545362539362542392545352542432538462545352538382538362545372542312542422e706e67)]
按操作对象分类结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vvvnKH22-1600397805199)(https://camo.githubusercontent.com/4a44e49ab13eacac26cbb0e481db73d6d11181b7/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d362f494f2d2545362539332538442545342542442539432545352541462542392545382542312541312545352538382538362545372542312542422e706e67)]
3.4.1.1既然有了字节流,为什么还要有字符流?
问题本质是想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么I/O流操作要分为字节流和字符流操作呢?
回答:字符流是由Java虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以,I/O流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话,使用字符流比较好。
3.4.1.2BIO,NIO,AIO有什么区别
- BIO(Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待期完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。因此,我们需要一种更高效的I/O处理模型来应对更高的并发量。
- NIO(Non-blocking/New I/O):NIO是一种同步非阻塞的I/O模型,在java1.4中引入NIO框架,对应java.nio包,提供了Channel,Selector,Buffer等抽象。NIO中的N可以理解为Non_blocking ,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发
- AIO(Asynchronous I/O):AIO也就是NIO2。在Java 7 中引入了NIO的改进版NIO 2 ,它是异步非阻塞的IO模型。异步IO是基于时间和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知 相应的线程进行后续的操作。AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行IO操作,IO操作本身是同步的。查阅相关资料,发现就目前来说AIO的应用还不是很广泛,Netty之前也查实过AIO,不过又放弃了。