面试题-java基础

常见字符集

ASII

美国人发明的数据集,用于输入到电脑中的代码,包括了英文和符号等。

标准的ASCII使用一个字节存储一个字符,首位是0,总共可以表示128个字符,对美国人来说完全够用。

但是对于中国人来说,还不够,则中国人也开发了一个自己的字符集,就是GBK

GBK

汉字占两个字节,英文占一个字节。

image-20221125202941749

GBK规定个汉字的第一个字节的第一位必须为1

image-20221125203408796

因为还有其他国家使用,所以如果其他国家也开发一个自己国家语言的字符集,就会出现好多字符集,当各个国家进行通信的时候就会出现乱码问题。因此国家开发组织就定义了一个标准编码:Unicode

Unicode

image-20221125203905637

UTF-8

image-20221125204141153

编码的时候可以根据汉字占3个字节进行编码,。但是解吗的时候怎么知道是3个字节代表的汉字呢,则下面utf-8提出了编码方式:

image-20221125204341276

总结

image-20221125204623833

image-20221125204642815

image-20221125204648335

Java代码对字符进行编码和解码

String提供的方法:

image-20221125205109693

image-20221125205136562

面向对象和面向过程

  • 面向过程是把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
  • 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题
  • 面向对象开发的程序一般更易维护,易复用,易扩展。

什么是多态?

在程序中定义的引用所指向的具体类型和通过该引用调用的方法在编译时期不能确定,而是在程序运行时期才能确定。由于是程序运行时才确定具体的类,这样不用修改源代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

多态:同一个方法,通过不同的对象调用产生不同的结果

多态的三个必要条件:

  • 有继承

  • 有重写

  • 父类引用指向子类对象

继承链中对象调用方法的优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。

注意!!!!:

指向子类的父类引用,由于向上转型了,它只能访问子类中重写父类的方法和属性,而对于父类中不存在的方法,该引用不能调用。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)

经典的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class A {  
public String show(D obj) {
return ("A and D");
}

public String show(A obj) {
return ("A and A");
}

}

public class B extends A{
public String show(B obj){
return ("B and B");
}

public String show(A obj){
return ("B and A");
}
}

public class C extends B{ }

public class D extends B{ }

public class Test {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();

System.out.println("1--" + a1.show(b)); //A and A
System.out.println("2--" + a1.show(c)); //A and A
System.out.println("3--" + a1.show(d)); //A and D
System.out.println("4--" + a2.show(b)); //4--B and A .首先a2是A引用,B实例,调用show(B b)方法,此方法在父类A中没有定义,所以B中方法show(B b)不会调用(多态必须父类中已定义该方法),再按优先级为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O),即先查this对象的父类,没有重头再查参数的父类。查找super.show((super)O)时,B中没有,再向上,找到A中show(A a),因此执行。

System.out.println("5--" + a2.show(c)); //同上
System.out.println("6--" + a2.show(d)); //A and D .查找B中没有show(D d)方法,再查A中,有,执行。
System.out.println("7--" + b.show(b)); //B and B
System.out.println("8--" + b.show(c)); //B and B .
System.out.println("9--" + b.show(d)); //A and D
}
}

抽象类和接口的区别?

抽象类:

  • 抽象类里面的全部方法,必须让子类全部实现,如果没有全部实现,则该子类也为抽象类,同样的接口也一样,如果子类没有全部实现接口中的方法,则该子类也为抽象类。
  • 抽象类中可以有普通方法,也可以有抽象方法,而抽象方法的个数可以是0个,也可以是多个。
  • 子类继承父类,必须重写全部的抽象方法,除非这个类也变成了抽象类。

接口:

  • 接口中所有方法都必须是抽象的
  • 接口中方法定义默认为public abstract类型,成员变量默认为public static final 类型。(为什么是final:如果不是final,意味着实现该接口的子类都可以去修改这个变量,这会影响其他继承该接口的类,。为什么是static:如果是非静态的变量,则实现了该接口的实现类都会有这个变量,由于接口是可以多继承的,如果另一个接口也有这样一个变量,则不知道用哪个。因此定为static,只能放在静态存储区)

主要区别:

  • 抽象类可以有构造方法,接口中不能有构造方法。(接口中所有的方法都是抽象的,不存在抽象的构造方法)
  • 抽象类中可以有普通方法,接口中所有方法都必须是抽象的。
  • 抽象类中抽象方法的访问类型可以是public,protected,但接口中抽象方法的访问类型只能是public,并且默认为public abstract
  • 抽象类中可以有静态方法(不能有静态抽象方法),接口中不能有静态方法。(因为接口不能被实例化,也就是不能分配内存空间,而static方法在实例化之前就已经分配了内存空间,所以矛盾了。)静态和抽象不能共存在一个方法上,静态不需要实例就可以运行,而抽象方法没有方法体,运行没有意义,所以不能共存。

重载和重写的区别

(1)重载:发生在同一个类中,方法名相同,参数类型不同、个数不同、顺序不同,返回值和访问修饰符可以不同。就是同一个方法能够根据输入数据的不同,做出不同的处理

(2)重写:发生在运行期,子类对父类的允许访问的方法实现过程进行重写。(抛出异常的范围小于等于父类,访问修饰符范围大于等于父类),其中构造方法不能被重写,⽗类⽅法访问修饰符为 private/final/static 则⼦类就不能重写该⽅法,但是被 static 修饰的⽅法能够被再次声明

JAVA中的泛型

使用泛型参数,编译器可以对泛型参数进行检测,并通过泛型参数可以指定传入的对象类型,是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。
也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,而这种参数类型可以用在类、方法和接口中,

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
//测试一下泛型的经典案例
ArrayList arrayList = new ArrayList();
arrayList.add("helloWorld");
arrayList.add("taiziyenezha");
arrayList.add(88);//由于集合没有做任何限定,任何类型都可以给其中存放
for (int i = 0; i < arrayList.size(); i++) {
//需求:打印每个字符串的长度,就要把对象转成String类型
String str = (String) arrayList.get(i);
System.out.println(str.length());
}
}

由于ArrayList可以存放任意类型的元素。例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,导致取出时强制转换为String类型后,引发了ClassCastException,因此程序崩溃了。

这显然不是我们所期望的,如果程序有潜在的错误,我们更期望在编译时被告知错误,而不是在运行时报异常。在jdk1.5后,泛型应运而生。让你在设计API时可以指定类或方法支持泛型,这样我们使用API的时候也变得更为简洁,并得到了编译时期的语法检查。

1
2
3
4
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("helloWorld");
arrayList.add("taiziyenezha");
arrayList.add(88);// 在编译阶段,编译器就会报错

这样可以避免了我们类型强转时出现异常。

使用泛型的好处

  • 避免了类型强转的麻烦。
  • 它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。

JAVA的跨平台

https://www.jb51.net/article/216499.htm

跨平台,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行

实现跨平台:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。(注意不是能在所有的平台上运行,关键是该平台是否能安装相应的虚拟机)

Java跨平台原理:
由源文件(.java)—>字节码文件(.class)(二进制文件)—–> 解释—->Unix,Win,Linux等机器。

简单的来说
就是当你需要执行某个Java程序时,会牵扯到JVM。具体就是我们编写的Java源码,编译后会生成一种.class文件,称为字节码文件。而Java虚拟机就是负责将字节码文件翻译成特定平台下的机器码,然后运行。

如下图

在这里插入图片描述

由此可知,JAVA的编译和执行与JVM有关,与平台无关。

而JVM分为很多个系统的版本,如Windows,Linux,macOS等等,都有其对应的JVM。

因此,
如果我们想要编译和执行编写好的Java程序,只需要在不同平台上安装其对应的JVM就行了。

如下图
在这里插入图片描述

基本类型和包装类型的区别

  • 包装类不赋值就是null,而基本类型有默认值,不是null。
  • 包装类可用作泛型,而基本类不可以。
  • 基本数据类型,如果是局部成员变量存放在Java虚拟机栈中,如果是成员变量存放在堆中,包装类对象属于对象类型存放在堆中。
  • 相比于对象类型,基本数据类型占用的空间非常小

包装类的缓存机制

Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False

1
2
3
4
5
6
7
8
9
10
11
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true

Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false

Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
1
2
3
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2); //输出false

Integer i1=40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是缓存中的对象。而Integer i2 = new Integer(40) 会直接创建新的对象。

记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较

image-20221003103940342

自动装箱和拆箱

装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。

因此,

  • Integer i = 10 等价于 Integer i = Integer.valueOf(10)
  • int n = i 等价于 int n = i.intValue();

为什么浮点数运算会有精度丢失的风险

1
2
由于计算机保存浮点数时是使用二进制进行存储的,而对于一些小数,不能准确的使用二进制存储,
因为可能无法消灭小数部分,导致无限循环下去,而又因为超过了其范围,所以就会导致精度丢失。
1
2
3
4
5
6
7
8
9
// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...

解决浮点数运算精度丢失问题

BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。

1
2
3
4
5
6
7
8
9
10
11
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);

System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */

BigDecimal进行等值比较的时候,要使用compareTo()方法,这是因为 equals() 方法不仅仅会比较值的大小(value)还会比较精度(scale),而 compareTo() 方法比较的时候会忽略精度。

1.0 的 scale 是 1,1 的 scale 是 0,因此 a.equals(b) 的结果是 false。

加减乘除的方法:

1
2
3
4
5
6
7
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.add(b));// 1.9
System.out.println(a.subtract(b));// 0.1
System.out.println(a.multiply(b));// 0.90
System.out.println(a.divide(b));// 无法除尽,抛出 ArithmeticException 异常
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));// 1.11

保留几位小数:

1
2
3
BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,RoundingMode.HALF_DOWN);
System.out.println(n);// 1.255

超过 long 整型的数据应该如何表示?

基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。

1
2
3
4
long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true

BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。

相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。

面向对象三大特征

封装

封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。

(就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。)

继承

不同类型的对象,相互之间经常有一定数量的共同点。将这些共同点定义为一个类,不同类型去继承这个类,

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。

注意三点:

  • 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  • 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法

多态

多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。

多态的特点:

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用“只在子类存在但在父类不存在”的方法;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

深拷贝和浅拷贝

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。新旧对象还是共用一个内存块。

    若是想要彻底的深拷贝,就要保证该对象的所有引用对象的类型都要去实现Cloneable接口,实现clone方法。

  • 深拷贝:是新建一个一模一样的对象,该对象与原对象不共享内存,修改新对象也不会影响原对象。

那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。

image-20221129200824038

obj2是对obj1的浅拷贝,obj3是对obj1的深拷贝

image-20221003135131615

阅读:https://blog.csdn.net/crystal_hhj/article/details/119740469

==和equals()的区别

==:对于基本数据类型比较的是值,对于引用类型比较的是两个的地址

equals():如果没有重写,则于==一样,如果重写了则比较的是内容。

字符串中intern 方法有什么作用?

String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:

  • 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
  • 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true

String 为什么是不可变的?

String 类中使用 final 关键字修饰字符数组来保存字符串

🐛 修正 : 我们知道被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。

String 真正不可变有下面几点原因:

  1. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

为什么要将String设置为不可变的

(1)字符串常量池需要String不可变,因为当创建一个String对象时,若此字符串已经存在常量池中,则不会创建一个新对象,而是引用已经存在的对象,如果允许改变的话,会影响另一个已经存在的对象。

(2)安全性,string被许多java类用来当作参数,若String可变,将会引起各种安全隐患。

(3)可以用作HashMap的key,因为通常建议把不可变的对象作为HashMap的key,以可缓存hashCode,对象一旦被创建hashCode的值也就不会改变,下次可以直接使用。

Exception 和 Error 有什么区别?

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
  • ErrorError 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

Hash冲突的解决方法

开放寻址法、链地址法(拉链法)、再哈希法、建立公共溢出区等方法。

(1)开放寻址法:

​ 开放寻址法又叫做开放定址法开地址法,从发生冲突的那个单元起,按照一定的次序,从哈希表中找到一个空闲的单元。然后把发生冲突的元素存入到该单元的一种方法。开放定址法需要的表长度要大于等于所需要存放的元素。

​ 开放定址法的缺点在于删除元素的时候不能真的删除,否则会引起查找错误,只能做一个特殊标记。只到有下个元素插入才能真正删除该元素。

(2)链地址法

​ 链地址法(Separate Chaining)的思路是将哈希值相同的元素构成一个同义词的单向链表,并将单向链表的头指针存放在哈希表的第 i 个单元中,查找、插入和删除主要在同义词链表中进行。链地址法适用于经常进行插入和删除的情况。

(3)再哈希法

​ 就是同时构造多个不同的哈希函数,当使用其中一个h哈希函数出现冲突后,再使用下一个哈希函数进行计算。直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。

(4)建立公共溢出区域

​ 将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

JAVA IO流

IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。

字符流和字节流的使用场景:

(1)字节流:主要用于处理图像、音频、视频、ppt等。它也可以处理文本文件

(2)字符流:主要用于处理纯文本的文件。不可以处理音频等文件

Java I/O模型详解

根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。

image-20221003215836718

用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间

从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。

java中三种常见的IO模型

https://www.cnblogs.com/crazymakercircle/p/10225159.html#autoid-h2-5-0-0

BIO(同步阻塞模型)

同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

最简单的方法,告诉内核需要哪些数据,然后等待,直到有了数据之后,从内核区拷贝到用户区,也就是所谓的阻塞IO。

image-20221003220126506

在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

NIO(同步非阻塞模型)

对于高负载、高并发的(网络)应用,应使用 NIO 。

告诉内核需要哪些数据,不等待,过一段时间去检查下,是否有数据,有则拷贝,没有则继续等待检查,这就是非阻塞IO。

image-20221003220214067

同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。

但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

IO多路复用

一般用在,想要设置一个高性能的网络服务器,这个服务器可以同时被多个客户端连接,并且能够处理这些客户端传上来的请求。

这个时候,I/O 多路复用模型 就上场了。

同时监视多个IO端口,只要有数据就处理。这就是IO多路复用。

image-20221003220255525

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。

使用场景:

1.当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。

2.当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。

3.如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。

4.如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。

5.如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

实现方法有:select 、poll或者epoll

(1)select:

首先创建一个socket,可以接收多个客户端的连接,**每个连接就是一个文件描述符(fd)**。文件描述符集合fds[]中每一个元素其实是一个随机的数字,代表这个文件描述符的编号,max用来保存其中最大的一个数字。

select执行步骤:

将rset拷贝一份到内核中,由内核判断哪个fd中有数据,如果一个或多个有数据就将其标记一下(修改bitmap)然后返回,用户进程通过遍历文件描述符集合得到数据。(由于bitmap会被修改,所以rset每次都会被重置)

1
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *excepset, const struct timeval *timeout);

返回值:就绪描述符的数目,超时返回0,出错返回-1

image-20230416162132765

缺点:

  • 对待测试的描述符个数有限制,最大1024
  • fdset不可重复适用,每次都要重新创建fd_set
  • 每次调用select,都会将readset、writeset、excepset从用户区拷贝到内核区,同样,返回时,又会将三个数据结构从内核区拷贝到用户区,大量的空间复制。
  • select返回时,并不知道是哪些fd,这是还要进行O(n)的循环判断,进行处理。

优点:用户态替我们监听fd

(2)poll

步骤:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

1
int poll(struct pollfd *fds, unsigned int nfds, int timeout);

image-20230416163041672

poll是对select的改进。

改进点

  • 没有最大个数的限制
  • 不用重新设置fd_set,因为换成了pollfd结构体,改变的是结构体中的revents,在poll返回后又将revents设置为0,因此可以重用。

select的其他缺点依然存在:

  • 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
  • select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

(3)epoll

优点:不需要再遍历所有的socket号来获取每一个socket的状态,只需要管理活跃的连接。即监听在通过epoll_create()创建的文件中注册的socket号以及对应的事件。只有产生就绪事件,才会处理,所以操作都是有效的,为O(1).

epoll_create 创建一个白板 存放fd_events
epoll_ctl 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上
epoll_wait 通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符

image-20230416164133893

两种触发模式:
LT:水平触发
当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。

ET:边缘触发
和 LT 模式不同的是,通知之后进程必须立即处理事件。下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数,

因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

AIO(异步模型)

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

image-20221003220332681

目前来说 AIO 的应用还不是很广泛。

为什么Java中只有值传递

先来介绍一下什么是值传递和引用传递:

  • 值传递 :方法接收的是实参值的拷贝,会创建副本。
  • 引用传递 :方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。

案例一

传递基本类型参数时,是将实参的值拷贝给形参,所以形参值的改变,不会影响实参值的改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

输出:

1
2
3
4
a = 20
b = 10
num1 = 10
num2 = 20

案例二

传递引用类型参数1:当传递引用类型时,是将实参中引用类型在堆中的地址拷贝给形参,

1
2
3
4
5
6
7
8
9
10
11
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
2
1
0

实际上,并不是的,这里传递的还是值,不过,这个值是实参的地址罢了!

也就是说 change 方法的参数拷贝的是 arr (实参)的地址,因此,它和 arr 指向的是同一个数组对象。这也就说明了为什么方法内部对形参的修改会影响到实参。

案例三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person {
private String name;
// 省略构造函数、Getter&Setter方法
}

public static void main(String[] args) {
Person xiaoZhang = new Person("小张");
Person xiaoLi = new Person("小李");
swap(xiaoZhang, xiaoLi);
System.out.println("xiaoZhang:" + xiaoZhang.getName());
System.out.println("xiaoLi:" + xiaoLi.getName());
}

public static void swap(Person person1, Person person2) {
Person temp = person1;
person1 = person2;
person2 = temp;
System.out.println("person1:" + person1.getName());
System.out.println("person2:" + person2.getName());
}

输出:

1
2
3
4
person1:小李
person2:小张
xiaoZhang:小张
xiaoLi:小李

swap 方法的参数 person1person2 只是拷贝的实参 xiaoZhangxiaoLi 的地址。因此, person1person2 的互换只是拷贝的两个地址的互换罢了,并不会影响到实参 xiaoZhangxiaoLi

Java反射中的方法

获取class示例的方法

1. 知道具体类的情况下可以使用:

1
Class alunbarClass = TargetObject.class;

但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化

2. 通过 Class.forName()传入类的全路径获取:

1
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");

3. 通过对象实例instance.getClass()获取:

1
2
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();

4. 通过类加载器xxxClassLoader.loadClass()传入类路径获取:

1
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");

反射中的方法

获取运行时类的方法结构

  • getMethods():获取当前运行时类及其所有父类中声明为public权限的方法
  • getDeclaredMethods():获取当前运行时类中声明的所有方法。(不包含父类中声明的方法)
  • getAnnotations():获取方法声明的注解
  • getModifiers():获取方法权限修饰符
  • getReturnType().getName():获取方法返回值类型
  • getName():获取方法名
  • public Class<?>[] getParameterTypes():取得全部的参数
  • public Class<?>[] getExceptionTypes():取得异常信息

获取当前运行时类的属性结构

  • getFields():获取当前运行时类及其父类中声明为public访问权限的属性
  • getDeclaredFields():获取当前运行时类中声明的所有属性。(不包含父类中声明的属性)
  • public int getModifiers(): 以整数形式返回此Field的修饰符
  • public Class<?> getType(): 得到Field的属性类型
  • public String getName(): 返回Field的名称。

获取构造器结构

  • getConstructors():获取当前运行时类中声明为public的构造器
  • getDeclaredConstructors():获取当前运行时类中声明的所有的构造器

getSuperclass();:获取运行时类的父类

getGenericSuperclass():获取运行时带泛型的父类

获取运行时类的带泛型的父类的泛型(重点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
获取运行时类的带泛型的父类的泛型
代码:逻辑性代码 vs 功能性代码
*/
@Test
public void test4(){
Class clazz = Person.class;

Type genericSuperclass = clazz.getGenericSuperclass();
ParameterizedType paramType = (ParameterizedType) genericSuperclass;
//获取泛型类型
Type[] actualTypeArguments = paramType.getActualTypeArguments();
// System.out.println(actualTypeArguments[0].getTypeName());
System.out.println(((Class)actualTypeArguments[0]).getName());
}

如何操作运行时类中的指定的属性 – 需要掌握

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
如何操作运行时类中的指定的属性 -- 需要掌握
*/
@Test
public void testField1() throws Exception {
Class clazz = Person.class;

//创建运行时类的对象
Person p = (Person) clazz.newInstance();

//1. getDeclaredField(String fieldName):获取运行时类中指定变量名的属性
Field name = clazz.getDeclaredField("name");

//2.保证当前属性是可访问的
name.setAccessible(true);
//3.获取、设置指定对象的此属性值
name.set(p,"Tom");

System.out.println(name.get(p));
}

如何操作运行时类中的指定的方法 – 需要掌握

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
如何操作运行时类中的指定的方法 -- 需要掌握
*/
@Test
public void testMethod() throws Exception {

Class clazz = Person.class;

//创建运行时类的对象
Person p = (Person) clazz.newInstance();

/*
1.获取指定的某个方法
getDeclaredMethod():参数1 :指明获取的方法的名称 参数2:指明获取的方法的形参列表
*/
Method show = clazz.getDeclaredMethod("show", String.class);
//2.保证当前方法是可访问的
show.setAccessible(true);

/*
3. 调用方法的invoke():参数1:方法的调用者 参数2:给方法形参赋值的实参
invoke()的返回值即为对应类中调用的方法的返回值。
*/
Object returnValue = show.invoke(p,"CHN"); //String nation = p.show("CHN");
System.out.println(returnValue);

System.out.println("*************如何调用静态方法*****************");

// private static void showDesc()

Method showDesc = clazz.getDeclaredMethod("showDesc");
showDesc.setAccessible(true);
//如果调用的运行时类中的方法没有返回值,则此invoke()返回null
// Object returnVal = showDesc.invoke(null);
Object returnVal = showDesc.invoke(Person.class);
System.out.println(returnVal);//null

}

如何调用运行时类中的指定的构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testConstructor() throws Exception {
Class clazz = Person.class;

//private Person(String name)
/*
1.获取指定的构造器
getDeclaredConstructor():参数:指明构造器的参数列表
*/

Constructor constructor = clazz.getDeclaredConstructor(String.class);

//2.保证此构造器是可访问的
constructor.setAccessible(true);

//3.调用此构造器创建运行时类的对象
Person per = (Person) constructor.newInstance("Tom");
System.out.println(per);

}

反射的应用

JDBC中的加载数据库驱动程序,Spring框架中加载bean对象,以及态代理,这些都使用到反射,因为我们要想理解一些框架的底层原理,反射是我们必须要掌握的

Java 8 的新特性

什么是函数式接口

概念:一个接口中的抽象方法只有一个,那么这个接口就是一个函数式接口。

通过注解检测一个接口是否是函数式接口:@FunctionallInterface

这是lambda表达式使用的前提。

Lambda表达式

lambda表达式是一个匿名函数,java 8允许把函数作为参数进行传递进方法中,(前提这个函数是函数式接口)

Java中的锁

悲观锁

在对数据进行操作的时候,认为该数据一定有其他线程对它进行修改,因此在获取数据的时候会加锁,确保数据不会被其他线程修改,常见的使用悲观锁的机制为:sychronizedlock

乐观锁

在操作数据的时候 ,认为该数据不会被其他线程所修改,所以不会加锁,而是在更新的时候判断数据有没有被其他线程更新,如果数据没有被更新则成功写入,否则进行重试或抛出错误。常见的乐观锁是CAS版本控制

CAS介绍:

  • CAS算法涉及到3个操作数:1、需要读写的内存值V;2、进行比较的值A;3、要写入的新值B。
  • 当且仅当V的值等于A的值,CAS通过原子方式用B来更新V的值(比较+更新是一个原子操作)。否则不会执行。

image-20220716231720069

CAS存在的问题:

  • ABA问题:一个线程把数据A变成了B,然后又重新变成了A,此时另一个线程读取该数据的时候,发现A没有变化,就误认为是原来的那个A,但是此时A的一些属性或状态已经发生过变化。

    解决办法:可以增加版本号(AtomicStampedReference对象),内存值每次修改后,版本号+1。https://blog.csdn.net/weixin_42671172/article/details/108340791。或者使用AtomicMarkableReference对象,判断修改状态是否一致。

  • 循环时间长开销大问题: CAS如果长时间不成功,就会导致一直自旋,给CPU带来很大的消耗。

  • 只能保证一个共享变量的原子操作: 对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

两个锁适用的场景:

  • 悲观锁适合写多的操作,先加锁可以确保数据写操作时的准确性
  • 乐观锁适合读多的操作,不加锁的特点能够使读操作的性能得到提升

自旋锁和适应性自旋锁

自旋锁:就是让后面请求锁的线程不放弃CPU的时间,进行自旋等待,等前面的线程释放了锁,当前线程就可以获取锁了,这种省略了CPU对线程状态的转变,避免切换线程的开销。

因为唤醒和阻塞一个Java线程需要操作系统切换CPU的状态来完成。如果同步资源的锁定时间过短,为了这一小段的时间切换线程,线程挂起和恢复会让系统得不偿失。如果让两个或两个以上的线程同时并发执行,我们可以让后面那个请求锁的线程不放弃CPU执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁执行流程:
某个线程尝试获取同步资源锁失败,资源被占用—>自旋锁—>不放弃CPU时间片,通过自旋进行等待—>再次尝试获取锁,获取成功—>获取同步资源。
非自旋锁的执行流程:
某个线程尝试获取同步资源锁失败,资源被占用—>非自旋锁—>CPU切换状态,使当前线程进行休眠—>CPU切换线程执行其他操作—>占用同步资源的线程释放了的锁—>恢复现场—>再次尝试获取锁。

img

Java对象头

在说sychronized优化锁之前了解一下Java对象头,

对象在内存中的布局分为三块区域:对象头,实例数据和对齐填充。

对象头中包含两部分:MarkWord 和 类型指针。如果是数组对象的话,对象头还有一部分是存储数组的长度。

MarkWord:用于存储对象自身的运行时数据,如 HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等。

类型指针:虚拟机通过这个指针确定该对象是哪个类的实例。

对象头的长度:

长度 内容 说明
32/64bit MarkWord 存储对象的hashCode或锁信息等
32/64bit Class Metadada Address 存储对象类型数据的指针
32/64bit Array Length 数组的长度(如果当前对象是数组)

****锁升级功能主要是依赖Mark Word中**同步锁标志位**和**是否偏向锁标志位**来实现的****。

image-20221128161846036

Sychronized优化锁

级别从低到高依次是:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

锁可以升级,但不能降级。即:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的。

无锁

25bit 4bit 1bit(是否是偏向锁) 2bit(锁标志位)
对象的hashCode 对象分代年龄 0 01

无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一资源,但同时只能有一个线程修改成功。

偏向锁

23bit 2bit 4bit 1bit 2bit
线程ID epoch 对象分代年龄 1 01

含义

当线程A第一次竞争到锁的时候,通过操作修改MarkWord中的偏向线程ID,锁变为偏向模式,如果不存在其他线程的竞争,那么持有偏向锁的线程将永远不需要进行同步(不需要反复的获得锁和释放锁消耗资源)

主要作用

不存在其他线程竞争的情况下,不需要反复的获得和释放锁,减少性能的消耗

场景

有且只有一个线程访问的情况

注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

当有其他线程进行竞争的时候,才会发生释放锁,检查锁的偏向线程 D与当前线程ID是否一致,如果一致直接进入同步。

如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID,

竞争成功,表示之前的线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁:
竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程问公平竞争锁。

轻量级锁

如果线程B竞争线程A失败了,则将偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。

含义

多线程竞争,但是任一时刻最多只有一个线程竞争(也就是两个线程在竞争同一个资源)

场景

有两个线程来交替访问进行竞争

**主要目的:**在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞

重量级锁

如果自旋多次,则升级为重量级锁。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

含义

直接到操作系统级别,使用monitor管程

场景

多个线程来访问竞争

总结

首先要明确一点是引入这些锁是为了提高获取锁的效率, 要明白每种锁的使用场景, 比如偏向锁适合一个线程对一个锁的多次获取的情况; 轻量级锁适合锁执行体比较简单(即减少锁粒度或时间), 自旋一会儿就可以成功获取锁的情况.


面试题-java基础
http://example.com/2022/06/21/面试题/
作者
zlw
发布于
2022年6月21日
许可协议