本文内容基于《Java程序员面试笔试宝典》,何昊、薛鹏、叶向阳著。
1.3 为什么需要public static void main(String[] args)这个方法?
1.4 如何实现在main方法执行前输出“Hello World”?
3.2 break、continue和return有什么区别?
3.3 final、finally和finalize有什么区别?
5.2 "=="、euqals()和hashCode()有什么区别?
5.3 String、StringBuffer、StringBuilder和StringTokenizer有什么区别?
9.3 ArrayList、Vector和LinkedList有什么区别?
9.4 HashMap、Hashtable、TreeMap和WeakHashMap有什么区别?
9.5 Collection和Collections有什么区别?
10.1 什么是线程?它与进程有什么区别?为什么要使用多线程?
10.7 sleep()方法和yield()方法有什么区别?
11.4 Statement、PreparedStatement和CallableStatement有什么区别?
11.5 getString()与getObject()方法有什么区别?
1. 基本概念
1.1 Java语言有哪些优点?
- Java为纯面向对象的语言;
- 平台无关性;
- Java提供了很多内置的类库;
- 提供了对Web应用开发的支持;
- 具有较好的安全性(数组边界检测和Bytecode检验等)和健壮性(强类型机制、垃圾回收器、异常处理和安全检查机制等);
- 去除了C++语言中难以理解、容易混淆的特性(头文件、指针、结构、单元运算符重载、多重继承等)。
1.2 Java与C++有什么异同?
- Java为解释型语言,C++为编译型语言;
- Java为纯面向对象语言;
- Java中没有指针的概念;
- Java不支持多重继承,支持实现多个接口;
- Java提供了垃圾回收器,不需要开发人员去管理内存的分配;
- Java不支持运算符重载,C++支持;
- Java没有预处理器,C++支持预处理(头文件);
- Java不支持默认函数参数,C++支持;
- Java不提供goto语句,但是是保留关键字,C++支持;
- Java不支持自动强制类型转换,C++支持;
- Java具有平台无关性;
- Java提供对注释文档的内建支持;
- Java包含了一些标准库,C++则依靠一些非标准的、由其他厂商提供的库。
1.3 为什么需要public static void main(String[] args)这个方法?
main方法是Java程序的入口方法,JVM在运行程序时,会首先查找main方法。
main方法的返回值必须是void,并由static和public关键字修饰。
1.4 如何实现在main方法执行前输出“Hello World”?
在Java中,静态块在类被加载时就会调用,且静态块不管顺序如何,都会在main方法执行之前执行。
public class Test {
static {
System.out.println("Hello World1");
}
public static void main(String[] args) {
System.out.println("Hello World3");
}
static {
System.out.println("Hello World2");
}
}
1.5 Java程序初始化的顺序是怎样的?
Java程序的初始化一般遵循3个原则(优先级递减):
- 静态对象优先于非静态对象初始化,其中静态对象只初始化一次,非静态对象可能会初始化多次;
- 父类优先于子类进行初始化;
- 按照成员变量的定义顺序进行初始化。
总结就是:父类static{} -> 子类static{} -> 父类{} -> 父类constructor -> 子类{} -> 子类constructor。
1.6 Java中的作用域有哪些?
Java中,变量的类型由3种:
- 成员变量:成员变量的作用范围与类的实例化对象的作用范围相同。
- 静态变量:静态变量被所有实例共享。
- 局部变量:局部变量的可见性为它所在的花括号内。
另,访问控制符范围:
当前类 | 同包 | 不同包子类 | 不同包非子类 | |
public | 1 | 1 | 1 | 1 |
protected | 1 | 1 | 1 | 0 |
default | 1 | 1 | 0 | 0 |
private | 1 | 0 | 0 | 0 |
1.7 一个Java文件中是否可以定义多个类?
一个Java文件中可以定义多个类,但是最多只能有一个类被public修饰,并且这个类的类名与文件名必须相同。
若这个文件中没有public的类,则文件名随便是一个类的名字即可。
当用javac指令编译这个.java文件时,它会为每一个类生成一个对应的.class文件。
1.8 什么是构造函数?
构造函数用来在对象实例化时初始化对象的成员变量。
Java中,构造函数具有以下特点:
- 构造函数必须与类名相同,并且不能有返回值,不能为void;
- 每个类可以有多个构造函数,当没有提供构造函数时,编译器会提供一个没有参数的默认的构造函数;
- 构造函数可以有任意个参数;
- 构造函数总是伴随着new操作译器调用,且不能由程序的编写者直接调用,必须要由系统调用;
- 构造函数的主要作用是完成对象的初始化工作;
- 构造函数不能被继承,因此不能被覆盖,但可以被重载;
- 子类可以通过super关键字来显式地调用父类的构造函数,当父类没有提供无参数的构造函数时,子类的构造函数中必须显式地调用父类的构造函数。
1.9 为什么Java中有些接口没有任何方法?
接口是抽象方法定义的集合,也可以定义一些常量值,是一种特殊的抽象类。
接口中只包含方法的定义,没有方法的实现。
接口中的所有方法都是抽象的,接口中成员的作用域修饰符都是public,接口中常量值默认使用public static final修饰。
Java 8开始,接口中可以定义default方法和static方法。
在Java中,有些接口内部没有声明任何方法,也就是说,实现这些接口的类不需要重写任何方法,这些没有任何方法声明的接口叫做标识接口,仅仅充当一个标识的作用,用来表明实现它的类属于一个特定的类型。
1.10 Java中的clone方法有什么作用?
Java在处理基本数据类型时,都是采用值传递,传递的是输入参数的复制。除此之外的其他类型都是引用传递,传递的是对象的一个引用,对象除了在函数调用时是引用传递,在使用"="赋值时也采用引用传递。
当有如下需求:从某个已有的对象A创建出另外一个与A具有相同状态的对象B,并且对B的修改不会影响到A的状态。使用赋值操作是无法达到目的的,这时可以用clone方法。
clone方法是Object类提供的一个方法,作用是返回一个Object对象的复制,返回的是一个新的对象而不是一个引用。
使用clone方法的步骤:
- 实现clone的类首先要继承Cloneable接口;
- 在类中重写clone方法;
- 在clone方法中调用super.clone();
- 把浅复制的引用指向原型对象新的克隆体。
public class User implements Cloneable {
private int age;
private Date birthday;
public User() {
}
public User(int age, Date birthday) {
this.age = age;
this.birthday = birthday;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", birthday=" + birthday +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
User user1 = new User(23, new Date());
User user2 = (User) user1.clone();
System.out.println((user1 == user2) +
"\n" + user1 +
"\n" + user2 +
"\n" + (user1.getBirthday() == user2.getBirthday())
);
}
}
上述代码中的方式是浅复制:被复制对象的所有变量都含有原来对象相同的值,而所有对其他对象的引用仍然指向原来的对象。
深复制:被复制对象的所有变量都含有原来对象相同的值,除去那些引用其他对象的变量,那些引用其他对象的变量将指向被复制的新对象,而不再是原有的那些被引用的对象,如下面的代码:
public class User implements Cloneable {
private int age;
private Date birthday;
public User() {
}
public User(int age, Date birthday) {
this.age = age;
this.birthday = birthday;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", birthday=" + birthday +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
User user = (User) super.clone();
user.birthday = (Date) this.getBirthday().clone();
return user;
}
public static void main(String[] args) throws CloneNotSupportedException {
User user1 = new User(23, new Date());
User user2 = (User) user1.clone();
System.out.println((user1 == user2) +
"\n" + user1 +
"\n" + user2 +
"\n" + (user1.getBirthday() == user2.getBirthday())
);
}
}
1.11 什么是反射机制?
反射机制允许程序在运行时进行自我检查,同时也允许对其内部的成员进行操作。
反射机制的功能有:得到一个对象所属的类、获取一个类的所有成员变量和方法、在运行时创建对象、在运行时调用对象的方法。
public class ReflectTest {
public static void main(String[] args) {
try {
Class c = Class.forName("com.yeta.review.a.Sub");
c = ReflectTest.class;
c = new ReflectTest().getClass();
Base b = (Base) c.newInstance();
b.f();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Base {
public void f() {
System.out.println("Base");
}
}
class Sub extends Base {
@Override
public void f() {
System.out.println("Sub");
}
}
总共有3种方法可以获取到Class类,如上述代码所示:
- Class.forName("");
- 类名.Class;
- 实例.getClass()。
【注】Java创建对象的方式有几种?
- 通过new语句实例化对象;
- 通过反射机制创建对象;
- 通过clone方法创建对象;
- 通过反序列化创建对象。
1.12 package有什么作用?
package的宗旨是:把.java文件、.class文件以及其他resource文件有条理地组织,以供使用。
package的作用是:
- 提供多层命名空间,解决命名冲突;
- 对类按功能进行分类,使项目的组织更加清晰。
1.13 如何实现类似于C语言中函数指针的功能?
可以利用接口与类来实现:先定义一个接口,然后在接口中声明要调用的方法,接着实现这个接口,最后把这个实现类的一个对象作为参数传递给调用程序,调用程序通过这个参数来调用指定的函数,从而实现回调函数的功能。
public class FunctionPointerTest {
public static void func(int a, int b, IntCompare cmp) {
if (cmp.compare(a, b) == 1) {
System.out.println(a + " 大于 " + b);
} else if (cmp.compare(a, b) == -1) {
System.out.println(a + " 小于 " + b);
} else {
System.out.println(a + " 等于 " + b);
}
}
public static void main(String[] args) {
func(1, 2, new AnIntCompare());
}
}
interface IntCompare {
int compare(int a, int b);
}
class AnIntCompare implements IntCompare {
@Override
public int compare(int a, int b) {
if (a > b) {
return 1;
} else if (a < b) {
return -1;
} else {
return 0;
}
}
}
2. 面向对象技术
2.1 面向对象与面向过程有什么区别?
面向对象是把数据及对数据的操作方法放在一起,作为一个相互依存的整体,即对象。对同类对象抽象出其共性,即类,类中的大多数数据,只能被本类的方法进行处理。类通过一个简单的外部接口与外界发生关系,对象与对象之间通过消息进行通信。程序流程由用户在使用中决定。
面向过程是一种以事件为中心的开发方法,就是自顶向下顺序执行,逐步求精,其程序结构是按功能划分为若干个基本模块,这些模块形成一个树状结构,各模块之间的关系也比较简单,在功能上相对独立,每一个模块内部一般都是由顺序、选择和循环3种基本结构组成,其模块化实现的具体方法是使用子程序,而程序流程在写程序时就已经决定。
2.2 面向对象有哪些特征?
- 抽象
抽象就是忽略一个主题中与当前目标无关的那些方法,以便更充分地注意与当前目标有关的方面。
- 封装
封装是指将客观事务抽象成类,每个类对自身的数据和方法实行保护。
- 继承
继承是一种联接类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。
- 多态
多态是允许不同类的对象对同一消息作出响应。
2.3 面向对象的开发方式有什么优点?
- 较高的开发效率(通过继承和组合实现代码的重用);
- 保证软件的鲁棒性(重用在相关领域经过长期测试的代码);
- 保证软件的高可维护性(代码的可读性很好和成熟的设计模式)。
2.4 继承的特性?
- Java不支持多继承;
- 子类只能继承父类的非私有(public和protected)成员变量与方法;
- 当子类中定义的成员变量和父类中定义的成员变量同名时,子类中的成员变量会覆盖父类的成员变量,而不会继承;
- 当子类中的方法与父类中的方法有相同的函数签名时,子类将会覆盖父类的方法,而不是继承。
其中第2条标红,是因为从内存实现和反射的角度来看,子类是可以继承父类的所有方法和状态的,参考:
https://blog.csdn.net/techgfuture/article/details/39231951
https://blog.csdn.net/zxc_helloworld/article/details/77451195
2.5 组合和继承有什么区别?
组合是指在新类里面创建原有类的对象,重复利用已有类的功能,是has-a关系。
继承是允许根据其他类的实现来定义一个类的实现,是is-a关系。
2.6 多态的实现机制是什么?
多态表示当同一个操作作用在不同对象时,会有不同的语义,从而会产生不同的结果。
在Java中,多态有2种表现方式:
- 方法的重载(overload)
重载是指同一个类中有多个同名的方法,这些方法有不同的参数,因此在编译期就能确定到底调用哪个方法。
- 方法的覆盖(override):
覆盖是指子类可以覆盖父类的方法,在Java中,基类的引用变量不仅可以指向基类的实例对象,也可以指向其子类的实例对象,而程序调用的方法在运行期才动态绑定。
public class OverTest {
public static void main(String[] args) {
Father father = new Son(10);
father.f();
father.g();
System.out.println(father.i);
}
}
class Father {
public int i;
public Father() {
g();
}
public Father(int i) {
this.i = i;
}
public void f() {
System.out.println("Father f()");
}
public void g() {
System.out.println("Father g()");
}
}
class Son extends Father {
public int i;
public Son() {
}
public Son(int i) {
this.i = i;
}
@Override
public void f() {
System.out.println("Son f()");
}
@Override
public void g() {
System.out.println("Son g()");
}
}
上述代码中,Father和Son的构造方法提现了重载,Son中覆盖了Father中的两个方法f()和g()。分析下面代码的流程:
Father father = new Son(10);
father.f();
father.g();
System.out.println(father.i);
对象father的静态类型是Father,实际类型是Son。
第一步:执行new Son(10),先调用父类Father的构造方法;
第二步:父类Father的构造方法中调用g()方法,此时应该调用子类Son的g()方法,因为子类Son中覆盖了父类Father的g()方法,在运行期得知了对象father的实际类型是Son;
第三步:调用对象father的f()方法,与第二步同理;
第四步:调用对象father的g()方法,与第二步同理;
第五步:输出对象father的变量i,对象father的静态类型是Father,这是在编译期就确定了的,所以输出的是父类Father中的变量i。
2.7 重载和覆盖有什么区别?
使用重载的注意事项:
- 重载是通过不同的方法参数来区分的,例如不同的参数个数、不同的参数类型或不同的参数顺序;
- 不能通过方法的访问权限、返回值类型和抛出的异常类型来进行重载;
- 对于继承来说,如果基类方法的访问权限为private,那么就不能在派生类对其重载,如果派生类也定义了一个同名的函数,这是一个新的方法。
使用覆盖的注意事项:
- 派生类中的覆盖方法必须要和基类中被覆盖的方法有相同的函数名和参数;
- 派生类中的覆盖方法的返回值必须和基类中被覆盖的方法的返回值相同;
- 派生类中的覆盖方法所抛出的异常必须和基类中被覆盖的方法所抛出的异常一致;
- 基类中被覆盖的方法不能为private,否则其子类只是定义了一个方法,并没有对其覆盖。
重载和覆盖的区别:
- 覆盖是子类和父类之间的关系,是垂直关系,重载是同一个类中方法之间的关系,是水平关系;
- 覆盖只能由一个方法或只能由一对方法产生关系,重载是多个方法之间的关系;
- 覆盖要求参数列表相同,重载要求参数列表不同;
- 覆盖关系中,调用方法体是根据对象的类型来决定的,而重载关系是根据调用时的实参表与形参表来选择方法体的。
2.8 抽象类与接口有什么异同?
相同点:
- 都不能被实例化;
- 接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能被实例化。
不同点:
- Java 8之前,接口只有定义,其方法不能在接口中实现,只有实现接口的类才能实现接口中定义的方法,而抽象类可以有定义与实现即其方法可以在抽象类中被实现;
- 接口需要实现,但抽象类只能被继承,一个类可以实现多个接口,但只能继承一个抽象类;
- 接口中定义的成员变量默认为public static final,而且必须给其赋初值,其所有成员方法都是public abstract的,但抽象类的成员变量和非抽象的成员方法的访问修饰符没有限制,抽象方法不能用private、static、synchronized、native等修饰。
- 接口被用于实现比较常用的功能,便于日后维护或者添加删除方法,而抽象类更倾向于充当公共类的角色,不适用于日后重新对里面的代码进行修改。
另:
- 抽象类可以继承抽象类;
- 抽象类可以实现接口;
- 接口可以继承抽象类;
- 接口可以继承接口;
- 接口不可以实现接口。
2.9 内部类有哪些?
- 静态内部类
静态内部类可以不依赖于外部类实例而被实例化,只能访问外部类的静态成员和方法,可以定义静态或非静态的成员和方法。
- 成员内部类
成员内部类只能在外部类被实例化之后才能被实例化,可以访问外部类所有的成员和方法,不能定义静态的属性和方法。
- 局部内部类
局部内部类像局部变量一样,不能被public、protected、private以及static修饰,只能访问方法中定义为final类型的局部变量。
- 匿名内部类
匿名内部类不适用关键字class、extends、implements,没有构造方法,必须继承其他类或者实现其他接口,不能定义静态成员、方法、类,遵循所有局部内部类的规则。
public class InnerClassTest {
private int a1;
private static int a2;
private void func1() {};
private static void func2() {};
// 静态内部类
static class StaticInnerClass {
// 可以定义静态或非静态的成员和方法
private int b1;
private static int b2;
public void f1() {
// 只能访问外部类的静态成员和方法
//System.out.println(a1);
System.out.println(a2);
//func1();
func2();
}
public static void f2 () {}
}
// 成员内部类
class MemberInnerClass {
// 不能定义静态的属性和方法
private int b1;
//private static int b2;
public void f1() {
// 可以访问外部类所有的成员和方法
System.out.println(a1);
System.out.println(a2);
func1();
func2();
}
//public static void f2 () {}
}
public void func() {
int c1 = 0;
final int c2 = 0;
// 局部内部类
class LocalInnerClass {
public void f1() {
// Variable 'c1' is accessed from within inner class, needs to be final or effectively final
//c1++;
System.out.println(c1);
System.out.println(c2);
}
}
// 匿名内部类
new Thread(new Runnable() {
@Override
public void run() {}
}).start();
}
public static void main(String[] args) {
// 静态内部类可以不依赖于外部类实例而被实例化
StaticInnerClass sic = new StaticInnerClass();
// 成员内部类只能在外部类被实例化之后才能被实例化
MemberInnerClass mic = new InnerClassTest().new MemberInnerClass();
}
}
2.10 如何获取父类的类名?
public class FatherNameTest extends Thread {
public void printFatherName1() {
System.out.println(super.getClass().getName());
}
public void printFatherName2() {
System.out.println(this.getClass().getSuperclass().getName());
}
public static void main(String[] args) {
FatherNameTest fnt = new FatherNameTest();
fnt.printFatherName1();
fnt.printFatherName2();
}
}
第一个方法获取的是自己的名字的原因是:
getClass()方法在Object类中被定义为final与native,子类不能覆盖该方法,因此this.getClass()和super.getClass()最终都调用的是Object的getClass()方法,该方法返回此Object的运行时类。
2.11 this与super有什么区别?
在Java中,this用来指向当前实例对象,它的一个非常重要的作用是用来区分对象的成员变量与方法的形参。
super可以用来访问父类的方法或成员变量。
3. 关键字
3.1 变量命名有哪些规则?
在Java中,变量名、函数名、数组名统称为标识符,标识符只能由字母、数字、下划线和美元符号组成,并且标识符的第一个字符必须是字母或下划线或美元符号,并且标识符也不能包含空白字符。
3.2 break、continue和return有什么区别?
- break用于直接强行跳出当前循环,不再执行剩余代码;
- continue用于停止当次循环,回到循环起始处,进入下一次循环操作;
- return用于跳转,表示从一个方法返回到调用该方法的地方。
另,跳出多重循环:
public class BreakTest {
public static void main(String[] args) {
out:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break out;
}
}
}
}
}
3.3 final、finally和finalize有什么区别?
- final可用于声明属性不可变(引用)、方法不可被覆盖和类不可被继承;
- finally作为异常处理的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示这个语句块最终一定被执行,经常被用在需要释放资源的情况下;
- finalize是Object类的一个方法,在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其他资源的回收,例如关闭文件等,需要注意的是,一旦垃圾回收器准备号释放对象占用的空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收该对象占用的内存。
3.4 static关键字有哪些作用?
static关键字主要有2种作用:
- 为某特定数据类型或对象分配单一的存储空间,而与创建对象的个数无关;
- 实现某个方法或属性与类而不是对象关联在一起,也就是说在不创建对象的情况下就可以通过类来直接调用方法或使用类的属性。
static关键字有4种使用情况:
- static成员变量
static变量属于类,在内存中只有一个复制,所有实例都指向同一个内存地址,只要静态变量所在的类被加载,这个静态变量就会被分配空间,因此就可以被使用了。
- static成员方法
static方法是类的方法,不需要创建对象就可以被调用,并且不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和方法,因为当static方法被调用时,该类的对象可能还没被创建,即使已经被创建了,也无法确定调用哪个对象的方法。
【注】static实现单例模式
public class StaticTest {
private static StaticTest st = null;
private StaticTest() {
}
public static StaticTest getInstance() {
if (st == null) {
st = new StaticTest();
}
return st;
}
}
- static代码块
JVM在加载类时会执行static代码块,如果有多个static代码块,JVM将按顺序执行,static代码块常用来初始化静态变量,并且static代码块只会被执行一次。
- static内部类
参考2.9。
3.5 使用switch时有哪些注意事项?
switch语句用于多分支选择,switch(expr),其中expr只能是一个枚举常量或一个整数表达式(char, byte, short, int, Integer),Java 7开始支持String。
3.6 volatile有什么作用?
volatile是一个类型修饰符,用来修饰被不同线程访问和修改的变量,系统每次使用这个变量的时候都从对应的内存中提取,而不是缓存,所以所有的线程在任何时候看到的变量的值都是相同的。
volatile不能保证操作的原子性。
volatile会阻止编译器对代码的优化。
3.7 instanceof有什么作用?
判断一个引用类型的变量所指向的对象是否是一个类(或接口、抽象类、父类)的实例。
3.8 怎么处理精度问题?
4. 基本类型与运算
4.1 Java提供了哪些基本数据类型?
Java一共提供了8种原始的数据类型,除了这8种基本的数据类型外,其他类型都是引用类型。
数据类型 | 字节长度 | 范围 | 默认值 | 包装类 |
byte | 1 | [-128, 127] | 0 | Byte |
short | 2 | [-32768, 32767] | 0 | Short |
int | 4 | [-2^31, 2^31-1] | 0 | Integer |
long | 8 | (-2^63, 2^63-1) | 0L | Long |
float | 4 | 32位单精度 | 0.0F | Float |
double | 8 | 64位双精度 | 0.0 | Double |
char | 2 | Unicode [0, 65535] | u0000 | Character |
boolean | 1 | true, false | false | Boolean |
4.2 什么是不可变类?
不可变类是指当创建了这个类的实例后,就不允许修改它的值了。
在Java中,所有基本类型的包装类都是不可变类,String也是。
要创建一个不可变类需要遵循4个基本原则:
- 类中所有成员变量被private修饰;
- 类中没有写或者修改成员变量的方法;
- 确保类中所有方法不会被子类覆盖;
- 如果一个类成员不是不可变量,那么在成员初始化或者使用get()方法获取该成员变量时,需要通过clone方法来确保类的不可变性;
- 如果有必要,可以覆盖Object类的equals()和hashCode()方法;
- 在创建对象时需要初始化所有成员变量,最好提供一个带参数的构造方法。
Java设计很多不可变类是因为不可变类具有使用简单、线程安全、节省内存等优点。
4.3 值传递和引用传递有哪些区别?
Java中,原始数据类型在传递参数时是按值传递,包装类型在传递参数时是按引用传递。
参考以下代码:
public class CallTest {
public static void change(StringBuffer ss1, StringBuffer ss2) {
ss1.append(" World");
ss2 = ss1;
}
public static void main(String[] args) {
Integer a = 1;
Integer b = a;
b++;
System.out.println(a + " & " + b);
StringBuffer s1 = new StringBuffer("Hello");
StringBuffer s2 = new StringBuffer("Hello");
change(s1, s2);
System.out.println(s1 + " & " + s2);
}
}
4.4 不同数据类型的转换有哪些规则?
类型转换的2种类型:
- 类型自动转换
低级数据类型可以自动转换为高级数据类型:byte < short < char < int < long < float < double。
char类型的数据转换为更高级的类型会转换为其对应的ASCII码。
byte、char、short类型的数据在参与运算时会自动转换为int型,但当使用"+="运算时,不会产生类型的转换。
boolean与其他基本类型之间不能相互转换。
- 强制类型转换
从高级数据类型转换为低级数据类型,可能会损失精度。
4.5 运算符优先级是什么?
优先级 | 运算符 |
1 | . () [] |
2 | +(正) -(负) ++ -- ~ ! |
3 | * / % |
4 | +(加) -(减) |
5 | << >> >>> |
6 | < <= > >= instanceof |
7 | == != |
8 | & |
9 | | |
10 | ^ |
11 | && |
12 | || |
13 | ?: |
14 | = += -= *= /= %= &= |= ^= ~= <<= >>= >>>= |
4.6 如何实现无符号数的右移操作?
>>是有符号右移运算符,>>>是无符号右移运算符,将对象对应的二进制数右移指定的位数:
- >>右移时,若是正数,则在高位补0,若是负数,则在高位补1。
- >>>右移时,无论是正数还是负数,都在高位补0。
public class RightMoveTest {
public static void main(String[] args) {
int i = -4;
System.out.println("有符号右移1位前: " + Integer.toBinaryString(i));
i >>= 1;
System.out.println("有符号右移1位后: " + Integer.toBinaryString(i) + "\n");
i = -4;
System.out.println("无符号右移1位前: " + Integer.toBinaryString(i));
i >>>= 1;
System.out.println("无符号右移1位后: " + Integer.toBinaryString(i) + "\n");
short j = -4;
System.out.println("有符号右移1位前: " + Integer.toBinaryString(j));
j >>= 1;
System.out.println("有符号右移1位后: " + Integer.toBinaryString(j) + "\n");
j = -4;
System.out.println("无符号右移1位前: " + Integer.toBinaryString(j));
j >>>= 1;
System.out.println("无符号右移1位后: " + Integer.toBinaryString(j) + "\n");
}
}
short j = -4;无符号右移1位后结果为什么最高位是1而不是0呢?这是因为-short先转换为int高位补1,无符号右移高位补0,这时候是01111111 11111111 11111111 11111110,之后又转换为short,只取了低位2个字节,这时候将这个结果按int的二进制输出,高位补1,所以就是那个结果。
【注】<<运算符
左移n位表示原来的值乘2的n次方,左移运算没有有符号与无符号之分,都是在低位补0。
System.out.println(3 << 2 == 3 * (2 * 2)); //true
4.7 char类型变量是否可以存储一个中文汉字?
Java中,默认使用Unicode编码方式,每个字符占用2个字节,可以用来存储中文。
5. 字符串与数组
5.1 字符串创建与存储的机制是什么?
【注】只要是使用new就会生成新对象。
例如以下代码:
String s = new String("abc");
上述代码可分为3个部分:
- "abc":若在字符串常量池中不存在"abc",则会创建一个字符串常量"abc",并将其添加到字符串常量池中,若存在,则不会创建;
- new String():在堆上创建一个对象;
- String s = :创建上一步在堆上创建的对象的一个引用。
【问】new String("abc")创建了几个对象?
【答】一个或两个,"abc"如果在字符串常量池中存在,则不会创建,如果存在则创建,new String()创建一个。
5.2 "=="、euqals()和hashCode()有什么区别?
- "=="运算符用来比较两个变量的值是否相等,这个值指的是变量对应的内存中所存储的数值,用来比较两个基本类型的数据或两个引用变量是否相等;
- equals()方法是Object类的一个方法,Object类的equals()方法直接使用"=="运算符来比较两个对象,所以一般覆盖使用,比如String类的equals()方法是用于比较两个对象的内容是否相同,即堆中的内容是否相同;
- hashCode()方法也是Object类的一个方法,Object类的hashCode()方法返回对象在内存中地址转换成的一个int值;
- 一般来讲,equals()方法是给用户调用的,hashCode()方法用户一般不会去调用它;
- 一般在覆盖equals()方法的同时也要覆盖hashCode()方法,否则,就会违反Object.hashCode的通用约定,从而导致该类无法与所有基于hash的集合类结合在一起正常运行;
- 如果x.equals(y) == true,那么一定会有x.hashCode() == y.hashCode(),如果x.hashCode() != y.hashCode(),那么一定会有x.equals(y) == false,其他的就不一定。
5.3 String、StringBuffer、StringBuilder和StringTokenizer有什么区别?
- String用于字符串操作,是不可变类;
- StringBuffer也用于字符串操作,是可变类,并且是线程安全的;
- StringBuilder也用于字符串操作,是可变类,并且是线程不安全的;
- StringTokenizer是用来分割字符串的工具类。
StringTokenizer st = new StringTokenizer("Hello World");
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
【注】执行效率:StringBuilder > StringBuffer > String,但是一般操作的数据量少,应该优先使用String类,如果在多线程下操作大量数据,应该使用StringBuffer,如果在单线程下操作大量数据,应该使用StringBuilder。
5.4 Java中数组是不是对象?
数组是指具有相同类型的数据的集合,它们一般具有固定的长度,并且在内存中占据连续的空间。
在Java中,数据不仅有其自己的属性(如length),还有一些方法可以被调用(如clone),所以数组是对象。
5.5 数组的初始化方式有哪些?
- 一维数组:
int[] a1 = new int[5];
int[] a2 = {1, 2, 3, 4, 5};
int a3[];
a3 = new int[5];
int a4[];
a4 = new int[]{1, 2, 3, 4, 5};
-
二维数组:
int[][] a1 = {{1, 2}, {3, 4, 5}};
int[][] a2 = new int[2][];
a2[0] = new int[]{1, 2};
a2[1] = new int[]{3, 4, 5};
int a3[][];
int[] a4[];
声明二维数组时,其中[]必须为空,第二维的长度可以不同。
【注】在Java中,数组被创建后会根据数组存放的数据类型初始化成对应的初始值,数组在定义时并不会给数组元素分配存储空间,所以[]为空,在初始化时才必须为之分配空间。
5.6 length属性与length()方法有什么区别?
- length属性是数组的属性,用来获取数组的长度;
- length()方法是针对字符串的,用来计算字符串的长度;
- size()方法是针对泛型集合的,用来查看集合中有多少个元素。
6. 异常处理
6.1 finally块中的代码什么时候被执行?
在Java中,finally块的作用就是为了保证无论出现什么情况,里面的代码一定会被执行。
finally块中的代码是在try块或者catch块中的return语句之前执行的,这时候如果在finally块中对return的变量进行修改,如果该变量是基本类型,那么无任何影响,但是如果该变量是引用类型,那么return的是在finally块中改变后的对象。
如果try块或者catch块与finally块中都有return,那么finally块中的return语句将会覆盖别处的return语句。
整个过程就是:程序在执行到return时会首先将返回值存储在一个指定的位置,其次去执行finally块,最终再返回。
finally块中的代码不一定会被执行:
- 如果在进入try块之前就返回或者出现异常;
- 如果在try块或者catch块中程序退出。
6.2 异常处理的原理是什么?
异常是指程序运行时所发生的非正常情况或错误,当程序违反了语义规则时,JVM会将出现的错误表示为一个异常并抛出,这个异常可以在catch块中捕获处理,异常处理的目的就是为了提高程序的安全性和鲁棒性。
- Error:表示程序在运行期间出现了非常严重的错误,并且该错误是不可恢复的,由于这属于JVM层次的严重错误,因此这种错误是会导致程序终止执行的;
- Exception:表示可恢复的异常,是编译器可以捕捉到的;
- Checked Exception:都发生在编译阶段,Java编译器会强制程序去捕获此类型的异常;
- Runtime Exception:编译器没有强制对其进行捕获并处理,如果不对这种异常进行处理,当出现这种异常时,会由JVM来处理。
【注】Java异常处理用到了多态的概念,如果在异常处理过程中,先捕获了基类,然后再捕获子类,那么捕获子类的代码将永远不会被执行,所以应该先捕获子类,再捕获基类。
7. 输入输出流
7.1 Java IO流的实现机制是什么?
在Java中,输入和输出都被称为抽象的流,流可以被看作一组有序的字节集合,即数据在两设备之间的传输。
流的本质是数据传输,根据处理数据类型的不同,流可以分为两大类:
- 字节流:字节流以字节为单位,包含两个抽象类InputStream和OutputStream;
public class IOTest {
public static void main(String[] args) {
InputStream is = null;
OutputStream os = null;
File file = new File("files/IOTest.txt");
File fileCopy = new File("files/IOTestCopy.txt");
if (file.exists()) {
try {
if (!fileCopy.exists()) {
fileCopy.createNewFile();
}
is = new FileInputStream(file);
os = new FileOutputStream(fileCopy);
byte[] b = new byte[1024];
while (is.read(b) != -1) {
String s = new String(b);
System.out.println(s);
os.write(b);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- 字符流:字符流以字符为单位,根据码表映射字符,一次可以读多个字节,包含两个抽象类Reader和Writer;
public class IOTest {
public static void main(String[] args) {
BufferedReader br = null;
BufferedWriter bw = null;
File file = new File("files/IOTest.txt");
File fileCopy = new File("files/IOTestCopy.txt");
if (file.exists()) {
try {
if (!fileCopy.exists()) {
fileCopy.createNewFile();
}
br = new BufferedReader(new FileReader(file));
bw = new BufferedWriter(new FileWriter(fileCopy));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
bw.write(line + "\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (bw != null) {
bw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- 字节流和字符流的最主要的区别在于字节流在处理输入输出时不会用到缓存,而字符流用到了缓存。
7.2 管理文件和目录的类是什么?
Java通过File来管理文件和目录,通过该类不仅能够查看文件或目录的属性,而且还可以实现对文件或目录的创建、删除和重命名等操作。
7.3 Java Socket是什么?
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个双向链路的一端称为一个Socket。
Socket也称为套接字,可以用来实现不同虚拟机或不同计算机之间的通信。
在Java中,Socket可以分为面向连接的Socket通信协议和面向无连接的Socket通信协议。
任何一个Socket都是由IP地址和端口号唯一确定的。
class Server {
public static void main(String[] args) {
BufferedReader br = null;
PrintWriter pw = null;
try {
ServerSocket ss = new ServerSocket(2000);
Socket socket = ss.accept();
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream(), true);
String line = br.readLine();
pw.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (pw != null) {
pw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Client {
public static void main(String[] args) {
BufferedReader br = null;
PrintWriter pw = null;
try {
Socket socket = new Socket("localhost", 2000);
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream(), true);
pw.println("hello");
String line;
while (true) {
line = br.readLine();
if (line != null) {
break;
}
}
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (pw != null) {
pw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
7.4 NIO是什么?
在NIO(即非阻塞IO)出现之前,Java是通过传统的Socket来实现基本的网络通信功能的,如果客户端没有对服务端发起连接请求,那么服务端accept就会阻塞,如果连接成功,当数据还没有准备好时,read同样会阻塞,当有多个连接的时候,就需要采用多线程的方式,所以程序的运行效率非常低下,因此引入NIO来解决这个问题。
NIO通过Selector、Channel和Buffer来实现非阻塞的IO操作:
- Channel可以看作一个双向的非阻塞的通道,在通道的两边都可以进行数据的读写操作;
- Selector实现了用一个线程来管理多个通道,类似于一个观察者;
- Buffer用来保存数据。
7.5 什么是Java序列化?
- 序列化
在分布式环境下,当进行远程通信时,无论是何种类型的数据,都会以二进制序列的形式在网络上传送。
序列化是一种将对象以一串字节描述的过程,用于解决在对对象流进行读写操作时所引发的问题。
序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要时把该流读取出来重新构造一个相同的对象。
所有要实现序列化的类都必须实现Serializable接口。
如果一个类能够被序列化,那么它的子类也能够被序列化。
被static、transient(表示临时数据)修饰的成员不能被序列化。
序列化能实现深复制。
public class SerializableTest {
public static void main(String[] args) {
User user = new User("YETA", 23);
File file = new File("files/SerializableTest.txt");
//序列化
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//反序列化
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
user = (User) ois.readObject();
System.out.println(user);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient Integer age;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
反序列化是将流转换为对象,在过程中通过serialVersionUID来判断类的兼容性,最好显示地声明它,可以提高程序的运行效率、提高程序不同平台上的兼容性和增强程序各个版本的可兼容性。
- 外部序列化
外部序列化与序列化的区别在于序列化是内置的API,只需要实现Serializable接口,不需要编写任何代码就可以实现对象的序列化,然而外部序列化Externalizable接口中的方法必须要进行实现。
外部序列化可以作为只序列化部分属性的一种方法,另种方法是用transient修饰对应属性。
8. Java平台与内存管理
8.1 为什么说Java是平台独立性语言?
平台独立性是指可以在一个平台上编写和编译程序,而在其他平台上运行。保证Java具有平台独立性的机制是字节码和JVM。Java程序被编译后生成字节码,不同的硬件平台上会安装不同的JVM,由JVM来负责把字节码翻译成硬件平台能执行的代码。
JVM解释执行的过程分为3步:
- 代码的装入:由类加载器完成;
- 代码的检验:由字节码校验器进行检查;
- 代码的执行:
- 即时编译方式:解释器先将字节码编译成机器码,然后再执行机器码;
- 解释执行方式:解释器通过每次解释并执行一小段字节码,这是通常采用的方式。
8.2 JVM加载class文件的机制是什么?
当运行指定程序时,JVM会将编译生成的.class文件(字节码)按照需求和一定的规则加载到内存中,并组织称为一个完成的Java应用程序,这个加载过程由类加载器完成。
类加载的方式有2种:
- 隐式加载:程序在使用new等方式创建对象时,会隐式地调用类加载器把对应的类加载到JVM中;
- 显式加载:通过直接调用class.forName()的方法来把所需要的类加载到JVM中。
类加载的步骤有3步:
- 装载:根据查找路径找到相对应的class文件,然后导入;
- 链接:
- 检查:检查待加载的class文件的正确性;
- 准备:给类中的静态变量分配存储空间;
- 解析:将符号引用转换成直接引用;
- 初始化:对静态变量和静态代码块执行初始化工作。
Java的类加载器:
8.3 什么是GC?
在Java中,GC(垃圾回收)的主要作用是回收程序中不再使用的内存。Java提供了垃圾回收器来自动检测对象的作用域,可自动地把不再被使用的存储空间释放掉。
对对象而言,如果没有任何变量去引用它,那么该对象将不可能被程序访问,因此可以认为它是垃圾信息,可以被回收,只要有一个以上的变量引用该对象,该对象就不会被垃圾回收。
几种常用的垃圾回收算法:
8.4 Java是否存在内存泄漏问题?
内存泄漏是指一个不再被程序使用的对象或变量还在内存中占有存储空间。
一般来讲,内存泄漏有两种情况:一是在堆中申请的空间没有被释放,二是对象已不再被使用,但还仍然在内存中保留着。垃圾回收机制可以有效地解决第一种情况,因此内存泄漏主要是指第二种情况。
引起内存泄漏的原因:
- 静态集合类:静态集合类的生命周期与程序一致,那么容器中的对象在程序结束之前就不能被释放,从而造成内存泄漏;
- 各种连接:例如数据库连接,如果访问数据库的过程中,Connection、Statement和ResultSet不显示地关闭,将会造成大量对象无法被回收,从而造成内存泄漏;
- 监听器:在释放对象的同时如果不删除对应的监听器也可能造成内存泄漏;
- 变量不合理的作用域:如果一个变量定义的作用范围大于其使用范围,很可能会造成内存泄漏;
- 单例模式可能会造成内存泄漏。
8.5 Java中的堆和栈有什么区别?
在Java中,基本数据类型的变量和对象的引用变量的内存都分配在栈上,而引用类型的变量的内存分配在堆上或者常量池。
从作用方面来看,堆主要用来存放对象,栈主要用来执行程序。栈的存取速度更快,但栈的大小和生命周期必须是确定的,因此缺乏灵活性。而堆可以在运行时动态地分配内存,生存期不用提前告诉编译器,这也导致了其存取速度缓慢。
9. 容器
9.1 Java Collections框架是什么?
Java Collections框架中包含了大量集合接口以及这些接口的实现类和操作它们的算法。
9.2 什么是迭代器?
迭代器是一个对象,它的工作是遍历并选择序列中的对象。
public class CollectionsTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
在使用Iterator遍历容器的过程中,如果对容器进行增加或删除操作,就会改变容器中对象的数量,从而导致抛出异常。如果是删除,解决方法可以是在迭代过程中,将要删除的元素存入一个集合中,迭代完毕直接removeAll(),或者使用迭代器的remove()方法。
ListIterator只存在于List中,支持在迭代期间向List中添加或删除元素,并且可以在List中双向移动。
9.3 ArrayList、Vector和LinkedList有什么区别?
ArrayList和Vector都是基于Objcet[] array来实现的,查询数据快,增加、删除数据需要移动容器里的元素,所以比较慢,ArrayList的容量每次默认扩充为原来的1.5倍,Vector扩充为原来的2倍,ArrayList是线程不安全的,Vector是线程安全的。
LinkedList是基于双向链表来实现的,查询数据慢,增加、删除数据不需要移动容器里的元素,所以比较块,是线程不安全的。
9.4 HashMap、Hashtable、TreeMap和WeakHashMap有什么区别?
WeakHashMap | HashMap | TreeMap | Hashtable |
线程不安全 | 线程安全 | ||
key, value都可以为null | key不可以为null, value可以为null | key, value都不可以为null | |
Iterator | Enumeration | ||
默认大小16,一定是2的指数 | 默认大小11,扩展old*2+1 | ||
将保存的记录根据key排序 | |||
弱引用 | 强饮用 |
将HashMap同步的方式:
Map map = Collections.synchronizedMap(new HashMap<>());
9.5 Collection和Collections有什么区别?
Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,实现该接口的类主要是List和Set,该接口的设计目标是为各种具体的集合提供最大化的统一的操作方式。
Collections是针对集合类的一个包装类,它提供了一系列静态方法以实现对各种集合的搜索、排序、线程安全化等操作,其中大多数防范都是用来处理线性表。Collections类不能实例化,如同一个工具类。
10. 多线程
10.1 什么是线程?它与进程有什么区别?为什么要使用多线程?
线程是指程序在执行过程中,能够执行程序代码的一个执行单元。
进程是指一段正在执行的程序,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源,但是各个线程拥有自己的栈空间。
为什要使用多线程:
- 减少程序的响应时间;
- 与进程相比,线程的创建和切换开销更小;
- 多CPU或多核计算机本身就具有执行多线程的能力;
- 简化程序的结构,使程序便于理解和维护。
10.2 同步和异步有什么区别?
同步就是当多个线程需要访问同一个资源时,它们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用。
异步就是多个线程各执行各的,不关心其它线程的状态和行为。
10.3 如何实现Java多线程?
-
继承Thread类,重写run()方法;
-
实现Runnable接口,实现run()方法;
-
实现Callable接口,实现call()方法;
Callable可以在任务结束后提供一个返回值,还可以在call()方法中抛出异常,Future的get()会阻塞当前线程。
public class ThreadTest {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
myThread1.start();
Thread myThread2 = new Thread(new MyThread2());
myThread2.start();
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<String> future = threadPool.submit(new MyThread3());
try {
System.out.println(future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
class MyThread1 extends Thread {
@Override
public void run() {
//
}
}
class MyThread2 implements Runnable {
@Override
public void run() {
//
}
}
class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception {
return null;
}
}
10.4 run()方法和start()方法有什么区别?
调用start()方法可以启动一个线程,是线程进入就绪状态,等待被调度执行。
调用run()方法会当作一个普通函数来调用,是同步的。
10.5 多线程同步的实现方法有哪些?
10.6 sleep()方法和wait()方法有什么区别?
- 原理不同
sleep()方法是Thread类的方法,线程自己就可以醒过来,而wait()是Object类的方法,需要拥有同一对象锁的其他线程进行唤醒。
- 对锁的处理机制不同
sleep()方法不会释放锁,而wait()方法会释放锁。
- 使用区域不同
sleep()方法可以在任意地方使用,并且需要捕获InterruptedException异常,而wait()方法必须在synchronized方法或synchronized块中使用,不需要捕获异常。
10.7 sleep()方法和yield()方法有什么区别?
- sleep()方法给其他线程运行机会时不考虑线程的优先级,而yield()方法只会给相同或更高优先级的线程;
- 调用sleep()方法之后会进入阻塞状态,所以线程在调用sleep()方法指定的时间内肯定不会被再次执行,而yield()方法是使线程进入就绪状态,是可能马上又被执行的;
- sleep()方法需要捕获InterruptedException异常,而yield()方法不需要捕获异常;
- sleep()方法比yield()方法具有更好的可移植性。
10.8 终止线程的方法有哪些?
https://blog.csdn.net/qq_28958301/article/details/83146528#%E4%B8%80%E3%80%81%E5%9F%BA%E7%A1%80
10.9 synchronized与Lock有什么异同?
- 用法不一样
synchronized既可以加载方法上,也可以加载代码块上,是托管给JVM执行的,而Lock需要显示地指定起始位置和终止位置,是通过代码实现的,它有比synchronized更精确的线程语义。
- 性能不一样
在资源竞争不是很激烈的情况下,synchronized的性能要优于ReetrantLock,而在资源竞争很激烈的情况下,synchronized的性能会下降的非常快,ReetrantLock的性能基本保持不变。
- 锁机制不一样
synchronized获取多个锁时,必须以相反的顺序释放,并且是自动解锁,不会因为出了异常而导致锁没有被释放从而引发死锁,而Lock需要开发人员手动去释放,并且必须在finally块中释放,否则会引起死锁问题。
10.10 什么是守护线程?
守护线程是指程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。
如果用户线程已经全部退出运行,只剩下守护线程存在了,那么JVM就会退出。
守护线程一般具有较低的优先级。
当在一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此。
守护线程的一个典型的例子就是垃圾回收器。
10.11 join()方法的作用是什么?
在Java中,join()方法的作用是让调用该方法的线程在执行完run()方法后,再执行join方法后面的代码,就是同步执行。
11. Java数据库操作
11.1 如何通过JDBC访问数据库?
JDBC用于在Java程序中实现数据库操作功能,它提供了执行SQL语句、访问各种数据库的方法,并为各种不同的数据库提供统一的操作接口。
public class JDBCTest {
private static final String USER = "root";
private static final String PASSWORD = "root";
private static final String URL = "jdbc:mysql://localhost:3306/test";
private static final String DRIVER = "com.mysql.jdbc.Driver";
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USER, PASSWORD);
// Statement
statement = connection.createStatement();
statement.execute("DROP TABLE IF EXISTS user");
statement.execute("CREATE TABLE user (id int AUTO_INCREMENT PRIMARY KEY, name varchar(50) NOT NULL)");
statement.execute("INSERT INTO user(name) VALUES (\"YETA\"), (\"RAY\")");
resultSet = statement.executeQuery("SELECT * FROM user");
while (resultSet.next()) {
System.out.println(resultSet.getInt(1) + " " + resultSet.getString(2));
}
// PreparedStatement
preparedStatement = connection.prepareStatement("SELECT * FROM user WHERE id = ?");
preparedStatement.setInt(1, 2);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
System.out.println(resultSet.getInt(1) + " " + resultSet.getString(2));
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
try {
if (preparedStatement != null) {
preparedStatement.close();
}
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
11.2 JDBC处理事务采用什么方法?
一个事务是由一条或多条对数据库操作的SQL语句所组成的一个不可分割的工作单元,只有当事务中的所有操作都正常执行完了,整个事务才会被提交给数据库。
commit()方法表示完成对事务的提交,rollback()方法表示完成事务回滚,一般而言,事务默认操作是自动提交。
JDBC的事务隔离级别:
- TRANSACTION_NONE JDB:不支持事务;
- TRANSACTION_READ_UNCOMMITTED:未提交读;
- TRANSACTION_READ_COMMITTED:已提交读;
- TRANSACTION_REPEATABLE_READ:可重复读;
- TRANSACTION_SERIALIZABLE:可序列化。
对应MySQL的隔离级别:https://blog.csdn.net/qq_28958301/article/details/89217269#3.1%C2%A0%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB
11.3 Class.forName的作用是什么?
在Java中,任何类只有被装在到JVM中才能运行。Class.forName()的作用就是把类加载到JVM中,它返回一个与带有给定字符串名的类或接口相关联的Class对象,并且JVM会加载这个类,同时JVM会执行该类的静态代码段。
例如JDBC中,要求Driver类在使用前必须向DriverManager注册自己,当执行Class.forName("com.mysql.jdbc.Driver")时,JVM会加载对应类,并执行静态代码段,在静态代码段中注册到了DriverManager上。
11.4 Statement、PreparedStatement和CallableStatement有什么区别?
Statement用于执行不带参数的简单SQL语句,并返回它所生成结果的对象,每次执行SQL语句时,数据库都要编译该SQL语句。
PreparedStatement用于执行带参数的预编译SQL语句。
CallableStatement用来调用数据库中的存储过程,如果有输出参数要注册,说明是输出参数。
PreparedStatement比Statement具有以下优点:
- 效率更高;
- 代码可读性和可维护性更好;
- 安全性更好,能够预防SQL注入攻击。
11.5 getString()与getObject()方法有什么区别?
JDBC提供了getString()、getInt()和getData()等方法从ResultSet中获取数据,程序会一次性把数据都放到内存中,当数据量大到内存中放不下的时候会抛出异常。
而使用getObject()不会一次性将所有数据都读到内存中,每次调用时才从数据库中去获取数据。