1.普通方法和默认方法
在Java中,普通方法和默认方法的概念更加明确,尤其是在接口的上下文中。让我们更详细地探讨这两个概念。
普通方法
定义:普通方法是由类定义的,包含具体实现的实例方法或静态方法。
特点:
- 由类(非接口)中的具体实现提供。
- 必须在类中实现,不能在接口中提供具体实现(除非是默认方法)。
- 可以是实例方法(需要实例化类)或静态方法(可以通过类名直接调用)。
示例:
public class MyClass {
// 普通实例方法
public void regularMethod() {
System.out.println("This is a regular method");
}
// 普通静态方法
public static void staticMethod() {
System.out.println("This is a static method");
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.regularMethod(); // 调用实例方法
MyClass.staticMethod(); // 调用静态方法
}
}
默认方法
定义:默认方法是在接口中定义的方法,带有默认实现。默认方法允许接口在不破坏实现类的情况下添加新方法。
特点:
- 在接口中使用
default
关键字定义。 - 可以被实现接口的类重写。
- 提供接口演化的灵活性,而无需强制所有实现类都实现新方法。
示例:
interface MyInterface {
// 默认方法
default void defaultMethod() {
System.out.println("This is a default method");
}
// 抽象方法
void abstractMethod();
}
class MyClass implements MyInterface {
// 重写默认方法
@Override
public void defaultMethod() {
System.out.println("This is the overridden default method");
}
// 实现抽象方法
@Override
public void abstractMethod() {
System.out.println("This is the abstract method implementation");
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.defaultMethod(); // 调用重写的默认方法
obj.abstractMethod(); // 调用实现的抽象方法
// 也可以使用接口类型变量调用默认方法
MyInterface interfaceObj = new MyClass();
interfaceObj.defaultMethod(); // 调用重写的默认方法
interfaceObj.abstractMethod(); // 调用实现的抽象方法
}
}
public private default protect
public:
可见性:在所有地方都可见。任何其他类都可以访问 public
成员。
protected:
可见性:在同一包内以及不同包中的子类可见。包外的非子类无法访问 protected
成员。
default:
可见性:在同一包内可见。包外的类无法访问 default
成员。
private:
可见性:仅在定义它的类内部可见。其他类无法访问 private
成员。
this和supper
this
关键字
this
关键字用于引用当前对象的实例。
super
关键字用于引用当前对象的父类部分
构造方法
构造方法(Constructor)是一种特殊类型的方法,用于创建和初始化对象。在 Java 中,构造方法的主要作用是初始化对象的状态。
特点:
-
方法名与类名相同:构造方法的方法名必须与类名完全一致,包括大小写。
-
没有返回类型:构造方法没有返回类型,包括
void
。 -
可以重载:在同一个类中,可以定义多个构造方法,只要它们的参数列表不同即可(参数的类型、顺序、个数)。
-
自动调用:创建对象时会自动调用与之对应的构造方法来初始化对象。如果没有显式定义构造方法,编译器会自动生成一个默认的无参构造方法。
-
不能被继承:构造方法不能被继承,子类无法继承父类的构造方法。
构造方法的访问修饰符:构造方法可以使用 public、protected、private 或包级私有(默认,不写修饰符)等访问修饰符,用于控制构造方法的访问权限。
2.成员变量vs局部变量
在Java中,成员变量(也称为实例变量或字段)和局部变量是两种不同类型的变量,它们有不同的作用域和生命周期。了解它们的区别对于编写和理解Java程序非常重要。以下是成员变量和局部变量的详细对比和解释。
成员变量(实例变量)
定义:成员变量是定义在类内部,但在方法、构造方法或代码块之外的变量。
特点:
- 作用域:成员变量的作用域是整个类。它们可以在类的所有方法、构造方法和代码块中使用。
- 生命周期:成员变量的生命周期是整个对象的生命周期。它们在对象创建时被初始化,当对象被垃圾回收时被销毁。
- 默认值:成员变量有默认值(对于数值类型是0,对于布尔类型是false,对于引用类型是null)。
- 访问修饰符:成员变量可以使用访问修饰符(如public, private, protected)来控制访问权限。
示例:
public class Person {
// 成员变量
private String name;
private int age;
// 构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 方法
public void display() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
局部变量
定义:局部变量是在方法、构造方法或代码块内部定义的变量。
特点:
- 作用域:局部变量的作用域是它所在的方法、构造方法或代码块。它们只能在定义它们的代码块中使用。
- 生命周期:局部变量的生命周期是它所在的方法、构造方法或代码块的执行时间。当方法、构造方法或代码块执行完毕时,局部变量就会被销毁。
- 无默认值:局部变量没有默认值,必须显式初始化后才能使用。
- 访问修饰符:局部变量不能使用访问修饰符,它们的访问权限由它们所在的代码块决定。
示例:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void display() {
// 局部变量
String info = "Name: " + name + ", Age: " + age;
System.out.println(info);
}
public void calculateAgeInFuture(int years) {
// 局部变量
int futureAge = age + years;
System.out.println(name + " will be " + futureAge + " years old in " + years + " years.");
}
}
成员变量和局部变量的对比
特性 | 成员变量 | 局部变量 |
---|---|---|
作用域 | 整个类 | 定义它的代码块、方法或构造方法内 |
生命周期 | 对象的生命周期 | 方法、构造方法或代码块的执行时间 |
默认值 | 有默认值 | 没有默认值,必须显式初始化 |
访问修饰符 | 可以使用访问修饰符控制访问权限 | 不能使用访问修饰符 |
内存位置 | 堆内存 | 栈内存 |
示例代码
以下代码演示了成员变量和局部变量的使用:
public class Person {
// 成员变量
private String name;
private int age;
// 构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 方法
public void display() {
// 局部变量
String info = "Name: " + name + ", Age: " + age;
System.out.println(info);
}
public void calculateAgeInFuture(int years) {
// 局部变量
int futureAge = age + years;
System.out.println(name + " will be " + futureAge + " years old in " + years + " years.");
}
public static void main(String[] args) {
// 创建对象、类的实例化
Person p1 = new Person("杰克", 24);
// 通过对象调用方法
p1.display(); // 输出: Name: 杰克, Age: 24
p1.calculateAgeInFuture(5); // 输出: 杰克 will be 29 years old in 5 years.
}
}
3.抽象类VS接口
在Java中,抽象类和接口都是用于定义类的规范,促进代码的重用和灵活性,但它们有不同的用途和限制。以下是抽象类和接口的详细对比和解释。
抽象类
定义:抽象类是不能实例化的类,通常用来作为其他类的基类。抽象类可以包含抽象方法(没有方法体的方法)和具体方法(有方法体的方法)。
特点:
- 不能实例化:抽象类不能直接创建实例。
- 可以包含具体方法:抽象类可以包含方法实现。
- 可以有成员变量:抽象类可以有实例变量和静态变量。
- 继承:一个类只能继承一个抽象类(Java单继承)。
- 构造方法:抽象类可以有构造方法,用于被子类调用。
- 不可被final修饰
示例:
abstract class Animal {
protected String name;
// 抽象方法
public abstract void makeSound();
// 具体方法
public void eat() {
System.out.println(name + " is eating.");
}
}
class Dog extends Animal {
public Dog(String name) {
this.name = name;
}
@Override
public void makeSound() {
System.out.println(name + " says: Woof Woof");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Buddy");
dog.makeSound(); // 输出: Buddy says: Woof Woof
dog.eat(); // 输出: Buddy is eating.
}
}
接口
定义:接口是一组抽象方法的集合,接口不能包含具体方法(在Java 8之前)。从Java 8开始,接口可以包含默认方法和静态方法。
特点:
- 不能实例化:接口不能直接创建实例。
- 只包含抽象方法:在Java 8之前,接口只能包含抽象方法。从Java 8开始,可以包含默认方法和静态方法。
- 没有成员变量:接口不能包含实例变量,但可以包含常量(
public static final
)。 - 实现:一个类可以实现多个接口(Java多继承)。
- 没有构造方法:接口不能有构造方法。
示例:
interface Animal {
// 抽象方法
void makeSound();
// 默认方法 (Java 8+)
default void eat() {
System.out.println("This animal is eating.");
}
// 静态方法 (Java 8+)
static void breathe() {
System.out.println("This animal is breathing.");
}
}
class Dog implements Animal {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public void makeSound() {
System.out.println(name + " says: Woof Woof");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Buddy");
dog.makeSound(); // 输出: Buddy says: Woof Woof
dog.eat(); // 输出: This animal is eating.
Animal.breathe(); // 输出: This animal is breathing.
}
}
抽象类与接口的对比
特性 | 抽象类 | 接口 |
---|---|---|
实例化 | 不能实例化 | 不能实例化 |
方法 | 可以包含抽象方法和具体方法 | 只能包含抽象方法(Java 8+ 允许默认方法和静态方法) |
成员变量 | 可以包含成员变量 | 只能包含常量 |
继承 | 一个类只能继承一个抽象类 | 一个类可以实现多个接口 |
构造方法 | 可以有构造方法 | 不能有构造方法 |
访问修饰符 | 可以有任何访问修饰符的方法和变量 | 方法默认是public ,变量默认是public static final |
选择使用抽象类还是接口
-
使用抽象类:
- 当你需要共享代码或定义一些默认行为时。
- 当你需要声明非公共的字段或方法时。
- 当你需要定义构造方法时。
-
使用接口:
- 当你需要实现多继承时。
- 当你需要定义一种类型的行为,而不关注具体的实现时。
- 当你希望未来可能有多个不相关的类实现该接口时。
4.重载VS重写
在Java中,重载(Overloading)和重写(Overriding)是两种不同的概念,它们分别用于方法和类的继承层次结构中。以下是重载和重写的详细对比和解释。
重载(Overloading)
定义:重载是在同一个类中定义多个方法,它们具有相同的方法名但具有不同的参数列表(参数类型、参数个数或参数顺序不同)。编译器根据调用时的参数类型和个数来决定调用哪个重载方法。
特点:
- 方法名相同:重载的方法必须具有相同的方法名。
- 参数列表不同:重载的方法的参数列表必须不同,参数类型、个数或顺序至少有一项不同。
- 返回类型可以相同也可以不同:重载方法的返回类型可以相同也可以不同。
- 可以抛出不同的异常:重载方法可以抛出不同的异常。
- 在同一个类中:重载方法定义在同一个类中。
- 编译时多态(静态多态)
示例:
public class Calculator {
// 重载方法,参数为两个整数
public int add(int a, int b) {
return a + b;
}
// 重载方法,参数为三个整数
public int add(int a, int b, int c) {
return a + b + c;
}
// 重载方法,参数为两个浮点数
public double add(double a, double b) {
return a + b;
}
}
重写(Overriding)
定义:重写是子类重新定义(覆盖)从其父类继承的方法,具有相同的方法签名(方法名、参数列表和返回类型必须完全相同)。重写用于实现多态,子类可以根据需要重新实现继承自父类的方法。
特点:
- 方法签名相同:重写的方法必须与父类的方法具有相同的方法签名(方法名、参数列表和返回类型必须一致)。
- 子类可以抛出更少的异常或不抛出异常:子类重写的方法可以抛出更少的异常,或者不抛出异常,或者抛出父类方法抛出异常的子类异常。
- 在子类中重写:重写方法定义在子类中,覆盖从父类继承的方法。
- 运行时多态(动态多态)
示例:
class Animal {
public void makeSound() {
System.out.println("Animal is making a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog is barking");
}
}
重载与重写的对比
特性 | 重载 | 重写 |
---|---|---|
方法名 | 相同 | 相同 |
参数列表 | 不同(类型、个数、顺序至少有一项不同) | 相同 |
返回类型 | 可以相同也可以不同 | 必须相同 |
异常 | 可以抛出不同的异常 | 子类可以抛出更少的异常或不抛出异常 |
类中位置 | 同一个类中 | 子类中(覆盖从父类继承的方法) |
关联 | 编译时多态(根据参数列表决定调用哪个方法) | 运行时多态(根据对象类型决定调用父类还是子类方法) |
选择使用重载还是重写
-
使用重载:
- 当你需要在同一个类中定义多个功能相似但参数类型或个数不同的方法时。
- 当你想根据参数列表的不同提供不同的实现时。
-
使用重写:
- 当你想在子类中提供特定于子类的实现,而不是使用父类的默认实现时。
- 当你想实现运行时多态,根据对象的实际类型来调用方法时。
5.== VS equals
在Java中,==
和 equals()
是用来比较对象的两种不同方式,它们具有不同的作用和使用场景。
1. ==
运算符
==
是Java中的相等运算符,它用于比较两个对象的引用是否相同,即判断两个对象是否指向内存中的同一个地址。
- 作用:比较两个对象的引用是否相同。
- 使用场景:主要用于比较基本数据类型的值或者判断两个对象是否是同一个对象实例。
示例:
String s1 = "Hello";
String s2 = "Hello";
String s3 = new String("Hello");
System.out.println(s1 == s2); // true,s1和s2指向常量池中同一个字符串对象
System.out.println(s1 == s3); // false,s1和s3分别指向不同的字符串对象
在上面的示例中,==
比较的是变量 s1
、s2
和 s3
的引用,而不是它们的值。因此,s1 == s2
返回 true
是因为它们指向了常量池中的同一个字符串对象,而 s1 == s3
返回 false
是因为 s3
是一个新创建的对象。
2. equals()
方法
equals()
方法是Java中的一个方法,它用于比较两个对象的内容是否相同。在Java中,Object
类中的 equals()
方法的默认行为是使用 ==
运算符来比较两个对象的引用是否相同。但是,许多类(如 String
、Integer
等)会重写 equals()
方法,以便根据对象的内容来进行比较。
- 作用:比较两个对象的内容是否相同。
- 使用场景:通常用于比较类的实例是否代表相同的逻辑实体,例如比较字符串、整数等。
示例:
String s1 = "Hello";
String s2 = "Hello";
String s3 = new String("Hello");
System.out.println(s1.equals(s2)); // true,使用equals比较内容
System.out.println(s1.equals(s3)); // true,使用equals比较内容
在上面的示例中,equals()
方法被 String
类重写,用于比较两个字符串对象的内容是否相同。因此,无论是 s1.equals(s2)
还是 s1.equals(s3)
都返回 true
,因为它们的内容都是相同的。
总结
- 使用
==
运算符来比较对象时,比较的是对象的引用是否相同。 - 使用
equals()
方法来比较对象时,通常比较的是对象的内容是否相同,如果类没有重写equals()
方法,则和==
的效果相同(即比较引用)。 - 在实际开发中,根据具体的需求来选择使用
==
运算符或equals()
方法,通常情况下,对于字符串、包装类和自定义类的对象,应该使用equals()
方法来比较它们的内容是否相同。
6.string类的常用方法
在Java中,String
类是用来表示字符串的类,提供了丰富的方法来操作和处理字符串。以下是一些常用的String
类方法及其示例:
1. 创建字符串
-
直接赋值 String str = "Hello, World!";
-
使用构造方法:
char[] chars = {'H', 'e', 'l', 'l', 'o'};
String str = new String(chars); -
2.
length()
返回字符串的长度。
-
String str = "Hello, World!"; int length = str.length(); // length = 13
-
3.charAt(int index)
返回指定索引处的字符。
-
char ch = str.charAt(0); // ch = 'H'
4.
substring(int beginIndex)
,substring(int beginIndex, int endIndex)
返回从指定索引开始的子字符串,或者从指定的开始索引到结束索引之间的子字符串。
String sub1 = str.substring(7); // sub1 = "World!" String sub2 = str.substring(0, 5); // sub2 = "Hello"
-
5.
contains(CharSequence s)
判断字符串是否包含指定的字符序列。
5. contains(CharSequence s) 判断字符串是否包含指定的字符序列。
-
6.
indexOf(int ch)
,indexOf(String str)
返回指定字符或字符串在字符串中首次出现的索引。
-
没有返回-1
int index1 = str.indexOf('o'); // index1 = 4 int index2 = str.indexOf("World"); // index2 = 7
-
7.
lastIndexOf(int ch)
,lastIndexOf(String str)
返回指定字符或字符串在字符串中最后一次出现的索引。
-
没有返回-1
int lastIndex1 = str.lastIndexOf('o'); // lastIndex1 = 8 int lastIndex2 = str.lastIndexOf("World"); // lastIndex2 = 7
8.
startsWith(String prefix)
,endsWith(String suffix)
判断字符串是否以指定前缀开始或以指定后缀结束。
-
boolean starts = str.startsWith("Hello"); // starts = true boolean ends = str.endsWith("World!"); // ends = true
9.
toUpperCase()
,toLowerCase()
将字符串转换为全大写或全小写。
String upper = str.toUpperCase(); // upper = "HELLO, WORLD!" String lower = str.toLowerCase(); // lower = "hello, world!"
10.
trim()
去除字符串两端的空白字符。
String strWithSpaces = " Hello, World! "; String trimmed = strWithSpaces.trim(); // trimmed = "Hello, World!"
11.
replace(char oldChar, char newChar)
,replace(CharSequence target, CharSequence replacement)
替换字符串中的字符或子字符串。
String replaced1 = str.replace('o', 'a'); // replaced1 = "Hella, Warld!" String replaced2 = str.replace("World", "Java"); // replaced2 = "Hello, Java!"
12.
split(String regex)
根据指定的正则表达式将字符串分割为数组
String[] parts = str.split(", "); // parts = ["Hello", "World!"]
13.
equals(Object anObject)
,equalsIgnoreCase(String anotherString)
比较字符串内容是否相等,忽略大小写比较。
String str1 = "Hello"; String str2 = "hello"; boolean equals = str1.equals(str2); // equals = false boolean equalsIgnoreCase = str1.equalsIgnoreCase(str2); // equalsIgnoreCase = true
14.
valueOf(Object obj)
将其他类型转换为字符串。
int num = 42; String numStr = String.valueOf(num); // numStr = "42"
15.
join(CharSequence delimiter, CharSequence... elements)
将多个字符串用指定的分隔符连接起来。
String joined = String.join(", ", "Hello", "World", "Java"); // joined = "Hello, World, Java"
面向对象的特征
1. 封装(Encapsulation)
封装是指将对象的属性和方法绑定在一起,隐藏对象的内部实现细节,只暴露必要的接口。这样做的好处是,可以保护对象的数据不被外界随意修改,同时简化了对象的使用。
- 数据隐藏:通过访问修饰符(如
private
,protected
,public
)来控制属性和方法的访问权限。 - 接口公开:通过公开的接口(如
public
方法)与外界交互。
2. 继承(Inheritance)
继承是指一个类可以继承另一个类的属性和方法,从而实现代码的复用和扩展。被继承的类称为父类(或超类、基类),继承的类称为子类(或派生类)。
- 单继承:Java只支持单继承,一个类只能有一个直接父类。
- 多层继承:一个类可以有多个祖先类(即多层继承)。
3. 多态(Polymorphism)
多态是指一个方法可以有多种不同的实现。多态性通过方法重载(Overloading)和方法重写(Overriding)来实现。
- 方法重载:在同一个类中,同名方法可以有不同的参数列表。
- 方法重写:子类可以重写父类的方法,以提供特定实现。
4. 抽象(Abstraction)
抽象是指将对象的复杂实现隐藏起来,只保留其最重要的特性和行为。抽象可以通过抽象类和接口来实现。
- 抽象类:使用
abstract
关键字定义的类,可以包含抽象方法(没有实现的方法)和具体方法(有实现的方法)。抽象类不能实例化。 - 接口:使用
interface
关键字定义的接口,只包含抽象方法(在Java 8之后可以包含默认方法和静态方法)。接口可以多继承,类可以实现多个接口。
5. 组合(Composition)
组合是指将一个类的对象作为另一个类的成员变量来实现类之间的关系。这种关系通常被称为“has-a”关系,相对于继承(is-a关系),组合更灵活,可以动态地修改对象的行为。
单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。单例模式在需要全局唯一的对象时非常有用,例如,配置管理器、日志记录器等。
单例模式的实现方式
单例模式的实现方式有多种,以下是几种常见的实现方式:
1. 饿汉式(Eager Initialization)
饿汉式在类加载时就创建实例。它是线程安全的,但即使不使用该实例也会创建它,可能造成资源浪费。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
// 私有化构造方法,防止外部实例化
private Singleton() {}
// 提供公有静态方法,返回唯一实例
public static Singleton getInstance() {
return INSTANCE;
}
}
2. 懒汉式(Lazy Initialization)
懒汉式在第一次需要使用实例时才创建。它是非线程安全的,需要加锁来保证线程安全性。
public class Singleton {
private static Singleton instance;
// 私有化构造方法,防止外部实例化
private Singleton() {}
// 提供公有静态方法,返回唯一实例
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
单例模式的优缺点
优点:
- 唯一实例:保证系统中只有一个实例,减少内存开销。
- 全局访问点:提供全局访问点,方便访问唯一实例。
- 延迟初始化:某些实现方式支持延迟初始化,只有在第一次使用时才创建实例。
缺点:
- 并发问题:某些实现方式在多线程环境下可能会引起并发问题,需要额外处理。
- 测试困难:由于单例类难以模拟和替换,单元测试可能变得困难。
- 全局状态:单例类持有全局状态,可能导致代码耦合,违背单一职责原则。
工厂模式
工厂模式是一种创建型设计模式,主要用于创建对象。工厂模式通过定义一个创建对象的接口,让子类决定实例化哪个类,从而将对象的实例化推迟到子类。工厂模式分为三种:简单工厂模式、工厂方法模式和抽象工厂模式。
简单工厂模式
简单工厂模式通过一个工厂类,根据传入的参数决定创建哪种类型的对象。简单工厂模式并不属于设计模式中的23种标准模式,但在实际应用中非常常见。
// 产品接口
interface Product {
void use();
}
// 具体产品A
class ConcreteProductA implements Product {
public void use() {
System.out.println("Using Product A");
}
}
// 具体产品B
class ConcreteProductB implements Product {
public void use() {
System.out.println("Using Product B");
}
}
// 简单工厂类
class SimpleFactory {
public static Product createProduct(String type) {
if (type.equals("A")) {
return new ConcreteProductA();
} else if (type.equals("B")) {
return new ConcreteProductB();
}
return null;
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Product productA = SimpleFactory.createProduct("A");
productA.use();
Product productB = SimpleFactory.createProduct("B");
productB.use();
}
}
工厂方法模式
工厂方法模式定义了一个创建对象的接口,但将对象的创建延迟到子类。子类决定要实例化的类是哪一个。
// 产品接口
interface Product {
void use();
}
// 具体产品A
class ConcreteProductA implements Product {
public void use() {
System.out.println("Using Product A");
}
}
// 具体产品B
class ConcreteProductB implements Product {
public void use() {
System.out.println("Using Product B");
}
}
// 抽象工厂
interface Factory {
Product createProduct();
}
// 具体工厂A
class ConcreteFactoryA implements Factory {
public Product createProduct() {
return new ConcreteProductA();
}
}
// 具体工厂B
class ConcreteFactoryB implements Factory {
public Product createProduct() {
return new ConcreteProductB();
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Factory factoryA = new ConcreteFactoryA();
Product productA = factoryA.createProduct();
productA.use();
Factory factoryB = new ConcreteFactoryB();
Product productB = factoryB.createProduct();
productB.use();
}
}
抽象工厂模式
抽象工厂模式提供一个接口,用于创建一系列相关或互相依赖的对象,而无需指定具体类。它适用于产品家族(即一组相关的产品)的创建。
// 产品A接口
interface ProductA {
void use();
}
// 产品B接口
interface ProductB {
void eat();
}
// 具体产品A1
class ConcreteProductA1 implements ProductA {
public void use() {
System.out.println("Using Product A1");
}
}
// 具体产品A2
class ConcreteProductA2 implements ProductA {
public void use() {
System.out.println("Using Product A2");
}
}
// 具体产品B1
class ConcreteProductB1 implements ProductB {
public void eat() {
System.out.println("Eating Product B1");
}
}
// 具体产品B2
class ConcreteProductB2 implements ProductB {
public void eat() {
System.out.println("Eating Product B2");
}
}
// 抽象工厂
interface AbstractFactory {
ProductA createProductA();
ProductB createProductB();
}
// 具体工厂1
class ConcreteFactory1 implements AbstractFactory {
public ProductA createProductA() {
return new ConcreteProductA1();
}
public ProductB createProductB() {
return new ConcreteProductB1();
}
}
// 具体工厂2
class ConcreteFactory2 implements AbstractFactory {
public ProductA createProductA() {
return new ConcreteProductA2();
}
public ProductB createProductB() {
return new ConcreteProductB2();
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
AbstractFactory factory1 = new ConcreteFactory1();
ProductA productA1 = factory1.createProductA();
ProductB productB1 = factory1.createProductB();
productA1.use();
productB1.eat();
AbstractFactory factory2 = new ConcreteFactory2();
ProductA productA2 = factory2.createProductA();
ProductB productB2 = factory2.createProductB();
productA2.use();
productB2.eat();
}
}
适用场景
- 简单工厂模式:适用于创建对象的逻辑比较简单、变化不大的场景。
- 工厂方法模式:适用于创建对象的逻辑较复杂、需要灵活应对变化的场景。
- 抽象工厂模式:适用于创建一组相关或互相依赖的对象,且不需要指定具体类的场景。
优缺点
简单工厂模式:
- 优点:简化了对象的创建过程。
- 缺点:增加新产品需要修改工厂类,违反开闭原则。
工厂方法模式:
- 优点:符合开闭原则,增加新产品不需要修改现有代码。
- 缺点:增加了类的数量,增加了系统的复杂度。
抽象工厂模式:
- 优点:符合开闭原则,增加新产品族不需要修改现有代码。
- 缺点:增加了类的数量,增加了系统的复杂度,修改产品族时需要修改工厂接口。
原型模式(Prototype Pattern)
原型模式(Prototype Pattern)是一种创建型设计模式,它通过复制现有对象来创建新对象,而不是通过实例化一个类。原型模式允许一个对象再创建另一个可定制的对象,并且不需要知道如何创建的细节。原型模式主要用于需要创建大量相似对象的场景。
原型模式的实现
原型模式通常通过实现 Cloneable
接口和重写 clone
方法来实现。
原型接口
首先,定义一个原型接口或者抽象类,通常包含一个 clone
方法。
public interface Prototype extends Cloneable {
Prototype clone();
}
具体原型类
具体的原型类实现原型接口,并实现 clone
方法
public class ConcretePrototype implements Prototype {
private String field;
public ConcretePrototype(String field) {
this.field = field;
}
public void setField(String field) {
this.field = field;
}
public String getField() {
return field;
}
@Override
public Prototype clone() {
Prototype prototype = null;
try {
prototype = (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return prototype;
}
}
客户端代码
客户端代码使用具体原型类的 clone
方法来创建新对象。
适用场景
- 创建对象成本较高,需要大量类似对象时。
- 希望减少创建对象时的复杂性,提高性能时。
- 希望动态创建对象并根据运行时状态进行调整时。
优缺点
优点:
- 提高对象创建的性能。
- 简化对象创建的过程。
- 动态调整对象的状态。
缺点:
- 实现深拷贝较为复杂。
- 需要考虑对象中循环引用的问题。
- 对象的拷贝操作可能带来安全问题。
装饰器模式(Decorator Pattern)
装饰器模式(Decorator Pattern)是一种结构型设计模式,允许向一个现有对象添加新的功能,同时又不改变其结构。装饰器模式通过创建一个装饰类来包装原有的类,使得在不改变原类的情况下扩展其功能。
装饰器模式的角色
- Component(抽象组件):定义一个对象接口,可以给这些对象动态地添加职责。
- ConcreteComponent(具体组件):实现抽象组件的类,定义了具体的对象。
- Decorator(装饰器):实现抽象组件接口,持有一个抽象组件对象的引用。
- ConcreteDecorator(具体装饰器):扩展装饰器类,给对象动态地添加职责。
示例代码
1. 定义抽象组件
// 抽象组件
public interface Component {
void operation();
}
2. 定义具体组件
// 具体组件
public class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("ConcreteComponent operation");
}
}
3. 定义装饰器
// 装饰器
public abstract class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
component.operation();
}
}
4. 定义具体装饰器
// 具体装饰器A
public class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
addedBehavior();
}
private void addedBehavior() {
System.out.println("ConcreteDecoratorA added behavior");
}
}
// 具体装饰器B
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
addedBehavior();
}
private void addedBehavior() {
System.out.println("ConcreteDecoratorB added behavior");
}
}
5. 使用装饰器模式
public class Client {
public static void main(String[] args) {
Component component = new ConcreteComponent();
Component decoratorA = new ConcreteDecoratorA(component);
Component decoratorB = new ConcreteDecoratorB(decoratorA);
decoratorB.operation();
}
}
输出结果
ConcreteComponent operation
ConcreteDecoratorA added behavior
ConcreteDecoratorB added behavior
装饰器模式的优缺点
优点:
- 扩展性强:可以在不修改原有类的情况下,动态地扩展对象的功能。
- 灵活组合:可以使用多个具体装饰器组合出不同的行为。
- 遵循开闭原则:可以在系统运行时动态地增加功能。
缺点:
- 产生大量小对象:因为每一个具体装饰器都是一个小对象,可能导致系统中的小对象数量过多,增加了复杂性。
- 调试困难:由于装饰器的层次较多,可能会导致调试和排错变得困难。
适用场景
- 需要扩展一个类的功能:但不希望通过继承来实现,或者继承的方法会导致类的数量急剧增加。
- 在不修改原有代码的情况下:为对象添加额外的功能。
- 动态、灵活地增加职责:需要在对象创建后根据不同的情况动态地为对象添加功能。
命令模式(Command Pattern)
命令模式(Command Pattern)是一种行为型设计模式,它将请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。命令模式主要用于将发出请求的对象和执行请求的对象解耦,从而使得二者不直接交互。
命令模式的角色
- Command(命令):定义命令的接口,声明执行的方法。
- ConcreteCommand(具体命令):实现命令接口,定义命令具体执行的操作。
- Invoker(调用者):持有命令对象,并通过命令对象来执行请求。
- Receiver(接收者):具体执行请求的对象,真正实现命令逻辑的地方。
示例代码
1. 定义命令接口
// 命令接口
public interface Command {
void execute();
}
2. 定义具体命令
// 具体命令
public class ConcreteCommand implements Command {
private Receiver receiver;
public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.action();
}
}
3. 定义接收者
// 接收者
public class Receiver {
public void action() {
System.out.println("Receiver action executed");
}
}
4. 定义调用者
// 调用者
public class Invoker {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void executeCommand() {
command.execute();
}
}
5. 使用命令模式
public class Client {
public static void main(String[] args) {
Receiver receiver = new Receiver();
Command command = new ConcreteCommand(receiver);
Invoker invoker = new Invoker();
invoker.setCommand(command);
invoker.executeCommand();
}
}
输出结果
Receiver action executed
命令模式的优缺点
优点:
- 降低耦合:命令模式将请求的发送者和接收者解耦。
- 增加灵活性:可以动态地组合命令对象,增加命令的复用性。
- 支持撤销和重做:可以很容易地实现命令的撤销和重做功能。
- 扩展性强:新增命令很容易,无需修改现有代码,符合开闭原则。
缺点:
- 可能导致类爆炸:每一个具体命令类都需要实现一个命令接口,可能导致类的数量急剧增加。
- 增加复杂性:命令模式增加了系统设计的复杂性,因为它涉及更多的类和对象。
适用场景
- 需要对行为进行参数化:你需要根据不同的请求来配置对象。
- 需要排队、记录请求日志:你需要将请求排队执行或记录请求日志。
- 支持撤销和重做操作:需要支持命令的撤销和重做功能。
7.异常处理
异常处理是编程中重要的概念,它指的是在程序执行过程中可能出现的错误或异常情况的处理方式。在Java中,异常是指程序在运行时发生的意外情况,如除以零、空指针引用等。Java提供了一套强大的异常处理机制,使得程序可以在发生异常时,通过捕获、处理和抛出异常来提高程序的健壮性和可靠性。
异常的分类
Java中的异常主要分为两类:受检异常(Checked Exception)和非受检异常(Unchecked Exception)(也称为运行时异常)。
-
受检异常(Checked Exception):是指编译器要求必须处理的异常,即在代码中必须显式捕获或者声明抛出。常见的受检异常有
IOException
、SQLException
等。 -
非受检异常(Unchecked Exception):是指编译器不要求必须处理的异常,即可以选择捕获处理,也可以不处理。常见的非受检异常有
NullPointerException
、ArrayIndexOutOfBoundsException
等,它们通常是程序逻辑错误或者运行时环境导致的异常。
异常处理的关键字和机制
Java提供了以下几个关键字和机制用于异常处
-
try-catch-finally:
try
块用来捕获可能抛出异常的代码,catch
块用来处理捕获到的异常,finally
块用来执行清理操作,无论是否发生异常都会执行。 -
try { // 可能抛出异常的代码 } catch (ExceptionType1 e1) { // 处理 ExceptionType1 类型的异常 } catch (ExceptionType2 e2) { // 处理 ExceptionType2 类型的异常 } finally { // 清理代码,无论是否发生异常都会执行 }
throw:用于手动抛出异常,可以在方法中根据特定条件抛出自定义的异常。
-
if (someCondition) { throw new CustomException("Something went wrong"); }
throws:用于方法签名中声明可能抛出的受检异常,告诉调用者该方法可能会抛出哪些异常,调用者在调用该方法时必须处理或继续向上抛出。
public void readFile(String fileName) throws IOException { // 可能抛出IOException的代码 }
try-with-resources:用于自动管理资源,如文件流或数据库连接等,自动释放资源,避免手动关闭资源时可能出现的异常。
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) { // 使用br读取文件内容 } catch (IOException e) { // 处理IOException }
异常处理的最佳实践
- 捕获精确异常:尽量精确捕获特定类型的异常,避免捕获过宽的异常类型。
- 处理异常:根据业务逻辑合理处理异常,可以通过日志记录、向用户显示错误信息等方式。
- 避免捕获过多异常:避免在一个
catch
块中捕获多个不同类型的异常,应当分别处理每种异常类型。 - 清理资源:在
finally
块中释放资源,确保程序能够正确地释放占用的资源,避免资源泄露。
Throwable有两个直接的子类: Error、Exception。
- Error
- JVM内部的严重问题,比如资源不足等,无法恢复。
- 处理方式: 程序员不用处理
- Exception
- JVM通过处理还可回到正常执行流程,即:可恢复。
- 分RuntimeException和其他Exception,或者说分为非受检异常(unchecked exception)和受检异常(checked exception)。
- 使用建议:将checked exceptions用于可恢复的条件,将runtime exception用于编程的错误。
- Use checked exceptions for recoverable conditions and runtime exceptions for programming errors (Item 58 in 2nd edition)
- 使用建议:将checked exceptions用于可恢复的条件,将runtime exception用于编程的错误。
- RuntimeException(unchecked exception)
- 处理或者不处理都可以(不需try…catch…或在方法声明时throws)
- 其他Exception(checked exception)
- Java编译器要求程序必须捕获(try…catch)或声明抛出(方法声明时throws)这种异常。
为什么要对unchecked异常和checked异常进行区分?
编译器将检查你是否为所有的checked异常提供了异常处理机制,比如说我们使用Class.forName()来查找给定的字符串的class对象的时候,如果没有为这个方法提供异常处理,编译是无法通过的。
8.常见的异常
|----编译时异常:(受检异常)在执行javac.exe命令时,出现的异常。 |----- ClassNotFoundException |----- FileNotFoundException |----- IOException |----运行时异常:(非受检异常)在执行java.exe命令时,出现的异常。 |---- ArrayIndexOutOfBoundsException |---- NullPointerException |---- ClassCastException |---- NumberFormatException |---- InputMismatchException |---- ArithmeticException
运行时异常:
1. NullPointerException
描述:当程序尝试访问一个 null
对象的成员(如方法、字段等)时抛出。 示例:
String str = null;
str.length(); // 抛出 NullPointerException
2. ArrayIndexOutOfBoundsException
[əˈreɪ]
描述:当程序尝试访问数组中不存在的索引时抛出。 示例:
int[] arr = new int[3];
int num = arr[5]; // 抛出 ArrayIndexOutOfBoundsException
3. ClassCastException
描述:当程序尝试将一个对象强制转换为不是其实例的类型时抛出。 示例:
Object obj = new Integer(100);
String str = (String) obj; // 抛出 ClassCastException
4. NumberFormatException
描述:当程序尝试将一个字符串转换为数值类型,但该字符串无法被解析为数值时抛出。 示例:
String str = "abc";
int num = Integer.parseInt(str); // 抛出 NumberFormatException
5. IllegalArgumentException
描述:当传递给方法的参数不合法或不正确时抛出。 示例:
Thread t = new Thread();
t.setPriority(11); // 抛出 IllegalArgumentException,因为优先级超出范围
6. IllegalStateException
描述:当方法被调用的对象处于不合适的状态时抛出。 示例:
List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator();
iterator.remove(); // 抛出 IllegalStateException,因为没有调用 next() 方法
7. ArithmeticException
描述:当数学运算中发生错误条件时抛出,例如除以零。 示例:
int result = 10 / 0; // 抛出 ArithmeticException
编译时异常:
1. FileNotFoundException
描述:当试图打开一个不存在的文件时抛出,这是一个受检异常。 示例:
FileInputStream fis = new FileInputStream("nonexistentfile.txt"); // 抛出 FileNotFoundException
2. IOException
描述:这是一个通用的输入输出异常,通常是与文件和流操作相关的异常。 示例:
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
reader.readLine(); // 可能抛出 IOException
3. SQLException
描述:当数据库操作出现错误时抛出。 示例:
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/test");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM nonexistent_table"); // 可能抛出 SQLException
4. ClassNotFoundException
描述:当试图通过名称加载类但找不到相应类文件时抛出,这是一个受检异常。 示例:
Class.forName("com.nonexistent.Class"); // 抛出 ClassNotFoundException
12. NoSuchMethodException
描述:当试图访问某个类的不存在的方法时抛出,这是一个受检异常。 示例:
Method method = String.class.getMethod("nonexistentMethod"); // 抛出 NoSuchMethodException
13. InterruptedException
描述:当线程在等待、睡眠或其他情况下被中断时抛出,这是一个受检异常。 示例:
Thread.sleep(1000); // 可能抛出 InterruptedException
14. IndexOutOfBoundsException
描述:这是一个通用的越界异常,其他如 ArrayIndexOutOfBoundsException
和 StringIndexOutOfBoundsException
都继承自这个类。 示例:
List<String> list = new ArrayList<>(); list.get(1); // 抛出 IndexOutOfBoundsException
这些是Java中一些常见的异常。了解和处理这些异常对于编写健壮的Java程序非常重要。处理异常时,可以使用 try-catch
块捕获异常,并在适当的地方进行处理或记录日志。
9.内部类
java中的内部类(Inner Class)是定义在另一个类内部的类。内部类可以帮助你更好地组织代码,增加封装性和逻辑上的联系。内部类有几种类型,每种类型都有其独特的特点和用途。
内部类的分类
- 成员内部类:定义在另一个类的成员位置,作为该类的成员变量一样使用。
- 局部内部类:定义在一个方法或一个作用域内部。
- 匿名内部类:没有类名的内部类,一般用于简化代码。
- 静态内部类:使用
static
关键字修饰的内部类,可以独立于外部类实例进行创建。 -
类的成员之五:内部类 1. 什么是内部类? 将一个类A定义在另一个类B里面,里面的那个类A就称为`内部类(InnerClass)`,类B则称为`外部类(OuterClass)`。 2. 为什么需要内部类? 具体来说,当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个内部的完整的结构B又只为外部事物A 提供服务,不在其他地方单独使用,么整个内部的完整结构B最好使用内部那类。 总的来说,遵循`高内聚、低耦合`的面向对象开发原则。 3. 内部类使用举例: Thread类内部声明了State类,表示线程的生命周期 HashMap类中声明了Node类,表示封装的key和value 4. 内部类的分类:(参考变量的分类) > 成员内部类:直接声明在外部类的里面。 > 使用static修饰的:静态的成员内部类 > 不使用static修饰的:非静态的成员内部类 > 局部内部类:声明在方法内、构造器内、代码块内的内部类 > 匿名的局部内部类 > 非匿名的局部内部类
1. 成员内部类
成员内部类定义在外部类的成员位置上,可以访问外部类的所有成员变量和方法。
class OuterClass {
private String outerField = "Outer field";
class InnerClass {
void display() {
System.out.println("Accessing: " + outerField);
}
}
void createInnerInstance() {
InnerClass inner = new InnerClass();
inner.display();
}
}
public class Main {
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.createInnerInstance();
}
}
2. 局部内部类
局部内部类定义在方法或作用域内部,只在定义它的方法中可见。
class OuterClass {
void outerMethod() {
final String localVar = "Local variable";
class LocalInnerClass {
void display() {
System.out.println("Accessing: " + localVar);
}
}
LocalInnerClass localInner = new LocalInnerClass();
localInner.display();
}
}
public class Main {
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.outerMethod();
}
}
3. 匿名内部类
匿名内部类没有名字,通常用来简化代码,只用一次的类。
abstract class Animal {
abstract void makeSound();
}
public class Main {
public static void main(String[] args) {
Animal dog = new Animal() {
void makeSound() {
System.out.println("Woof");
}
};
dog.makeSound();
}
}
4. 静态内部类
静态内部类用 static
修饰,可以独立于外部类的实例进行创建,无法访问外部类的非静态成员。
class OuterClass {
private static String staticOuterField = "Static outer field";
static class StaticInnerClass {
void display() {
System.out.println("Accessing: " + staticOuterField);
}
}
}
public class Main {
public static void main(String[] args) {
OuterClass.StaticInnerClass staticInner = new OuterClass.StaticInnerClass();
staticInner.display();
}
}
使用内部类的好处
- 逻辑分组:将相关的类放在一起,增加代码的可读性。
- 封装:可以隐藏内部类,不让其他类访问它。
- 简化代码:特别是匿名内部类,可以简化只需要一次使用的类的代码。
注意事项
- 内部类可以访问外部类的成员,包括私有成员。
- 静态内部类不能访问外部类的非静态成员。
- 匿名内部类没有构造函数,因为它没有类名。
内部类提供了强大的工具来组织和封装代码,可以根据具体需求选择合适的内部类类型。
eg:
package com.atguigu09.inner;
/**
* ClassName: OuterClassTest
* Description:
*
* @Author 尚硅谷-宋红康
* @Create 10:31
* @Version 1.0
*/
public class OuterClassTest {
public static void main(String[] args) {
//1. 创建Person的静态的成员内部类的实例
Person.Dog dog = new Person.Dog();
dog.eat();
//2. 创建Person的非静态的成员内部类的实例
// Person.Bird bird = new Person.Bird(); //报错
Person p1 = new Person();
Person.Bird bird = p1.new Bird();//正确的
bird.eat();
bird.show("黄鹂");
bird.show1();
}
}
class Person{ //外部类
String name = "Tom";
int age = 1;
//静态的成员内部类
static class Dog{
public void eat(){
System.out.println("狗吃骨头");
}
}
//非静态的成员内部类
class Bird{
String name = "啄木鸟";
public void eat(){
System.out.println("鸟吃虫子");
}
public void show(String name){
System.out.println("age = " + age);//省略了Person.this
System.out.println("name = " + name);
System.out.println("name = " + this.name);
System.out.println("name = " + Person.this.name);
}
public void show1(){
eat();
this.eat();
Person.this.eat();
}
}
public void eat(){
System.out.println("人吃饭");
}
public void method(){
//局部内部类
class InnerClass1{
}
}
public Person(){
//局部内部类
class InnerClass1{
}
}
{
//局部内部类
class InnerClass1{
}
}
}
结果:
狗吃骨头
鸟吃虫子
age = 1
name = 黄鹂
name = 啄木鸟
name = Tom
鸟吃虫子
鸟吃虫子
人吃饭
静态方法
在 Java 中,静态方法(static
方法)是属于类而不是实例的方法。静态方法可以通过类名直接调用,而不需要创建类的实例。静态方法有一些特殊的规则和使用场景,下面详细介绍。
特点
-
属于类而非实例: 静态方法是属于类的,因此可以通过类名直接调用,而不需要创建实例。
-
无法访问实例变量和实例方法: 静态方法无法直接访问实例变量和实例方法,因为静态方法是在类加载时就存在的,而实例变量和实例方法是在对象创建时才存在的。静态方法只能访问静态变量和调用静态方法。
-
可以作为工具方法: 静态方法通常用来实现一些工具方法,如数学计算、字符串操作等。例如,
Math
类中的sqrt
、pow
方法都是静态方法。 -
静态块: 类中可以定义静态代码块,用于初始化静态变量。静态代码块在类加载时执行一次。
为什么静态方法不能调用非静态方法和变量
静态方法是属于类的,它们在类加载时就存在,而非静态方法和变量是属于对象的,它们在对象创建时才存在。因此,静态方法无法直接调用非静态方法和变量,因为在静态方法执行时,可能没有任何对象存在。
List、Set、Map 之间的区别是什么?
List:有序集合、元素可重复
Set:元素不可重复,HashSet无序,LinkedHashSet按照插入排序,SortedSet可排序
Map:键值对集合,存储键、值之间的映射。key无序,唯一,value可重复
HashMap 和 Hashtable 有什么区别?
HashMap不是线程安全的,HashTable是线程安全的
HashMap允许Null Key和Null Value,HashTable不允许
如何决定使用 HashMap 还是 TreeMap?
如果需要得到一个有序的结果应该使用TreeMap
如果不需要排序最好选用HashMap,性能更优
说一下 HashMap 的实现原理?
HashMap基于Hash算法实现,通过put(key,value)存储,get(key)来获取value
当传入key时,HashMap会根据key,调用Hash(Object key)方法,计算出Hash值,根据Hash值将Value保存在Node对象里,Node对象保存在数组里。
当计算出的Hash值相同时,称为Hash冲突,HashMap的做法是用链表和红黑树存储相同Hash值的value
当Hash冲突的个数:小于等于8使用链表,大于8使用红黑树解决链表查询慢的问题。
说一下 HashSet 的实现原理?
HashSet是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet的操作相对比较简单,相关HashSet的操作,基本上都是直接调用底层的HashMap的相关方法来完成,HashSet不允许有重复的值,并且元素是无序的
ArrayList 和 LinkedList 的区别是什么?
ArrayList的数据结构是动态数组;LinkedList的数据结构是双向链表。
ArrayList比LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据结构,需要依次往后查找
在非首尾的增删操作,LinkedList要比ArrayList的效率要高,因为ArrayList在操作增删时要影响其他元素的下标
总结:需要频繁读取集合中的元素时,推荐使用ArrayList;插入和删除操作较多时,推荐使用LinkedList
如何实现数组和 List 之间的转换?
List转数组:String[] list = List.toArray(array);//array为List
数组转List:List list = java.util.Arrays.asList(array);//array为数组
10.ArrayList 和 Vector 的区别是什么?
相同点:都实现了List接口,都是有序集合
区别:Vector是线程安全的,ArrayList不是线程安全的;
当Vector或ArrayList中的元素超过它的初始大小时,Vector会将容量翻倍,而ArrayList只会将容量扩大50%
Array 和 ArrayList 有何区别?
Array类型的变量在声明时必须实例化;ArrayList可以只是先声明;
Array大小是固定的,而ArrayList的大小是动态变化的;
Array可以包含基本类型和对象类型,ArrayList只能包含对象类型
在 Queue 中 poll()和 remove()有什么区别?
Queue中poll()和remove()都是用来从队列头部删除一个元素;
在队列元素为空的情况下,remove()方法会抛出NoSuchElementException异常,而poll()只会返回null
类的对象进行排序
要对类的对象进行排序,通常需要实现 Comparable
接口或使用 Comparator
来定义排序规则。这里给出两种常见的排序方法示例:
方法一:实现 Comparable 接口
-
实现 Comparable 接口:该接口定义了一个
compareTo
方法,用于比较当前对象和另一个对象的顺序。public class Person implements Comparable<Person> { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } // 实现 compareTo 方法,按照年龄升序排序 @Override public int compareTo(Person other) { return Integer.compare(this.age, other.age); } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } // 测试排序 public static void main(String[] args) { List<Person> people = new ArrayList<>(); people.add(new Person("Alice", 25)); people.add(new Person("Bob", 20)); people.add(new Person("Charlie", 30)); Collections.sort(people); // 使用 Collections.sort() 方法排序 for (Person person : people) { System.out.println(person); } } }
输出结果:
Person{name='Bob', age=20} Person{name='Alice', age=25} Person{name='Charlie', age=30}
使用 Collections.sort() 方法:通过调用
Collections.sort()
方法,可以对实现了Comparable
接口的类的对象进行排序。
方法二:使用 Comparator 接口
如果无法修改类的源代码或需要多种排序方式,可以使用 Comparator
接口来定义不同的比较规则。
import java.util.*;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
// 测试排序
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 25));
people.add(new Person("Bob", 20));
people.add(new Person("Charlie", 30));
// 使用 Comparator 匿名类定义排序规则(按照年龄降序排序)
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return Integer.compare(p2.getAge(), p1.getAge()); // 降序
}
});
for (Person person : people) {
System.out.println(person.getName() + " - " + person.getAge());
}
}
}
输出结果:
Charlie - 30
Alice - 25
Bob - 20
通过以上两种方法,可以对类的对象进行灵活的排序,根据实际需求选择实现 Comparable
接口或使用 Comparator
接口来定义排序逻辑。
java 中 IO 流分为几种?
在 Java 中,IO(输入/输出)流用于处理数据的读写操作。Java IO 流主要分为两大类:字节流和字符流。字节流处理原始的二进制数据,而字符流处理文本数据。每种流又分为输入流和输出流。
字节流
字节流用于读取和写入二进制数据,如图像、音频、视频等。字节流以字节为单位进行操作。
字节输入流(InputStream)
FileInputStream
:从文件中读取字节。ByteArrayInputStream
:从字节数组中读取字节。FilterInputStream
:为其他输入流提供附加功能。BufferedInputStream
:为输入流提供缓冲功能,提高读取效率。DataInputStream
:允许应用程序以机器无关方式从底层输入流中读取基本 Java 数据类型。
字节输出流(OutputStream)
FileOutputStream
:将字节写入文件。ByteArrayOutputStream
:将字节写入字节数组。FilterOutputStream
:为其他输出流提供附加功能。BufferedOutputStream
:为输出流提供缓冲功能,提高写入效率。DataOutputStream
:允许应用程序以机器无关方式将基本 Java 数据类型写入底层输出流。
字符流
字符流用于处理字符数据,能够自动处理字符编码转换。字符流以字符为单位进行操作。
字符输入流(Reader)
FileReader
:从文件中读取字符。CharArrayReader
:从字符数组中读取字符。StringReader
:从字符串中读取字符。BufferedReader
:为字符输入流提供缓冲功能,提高读取效率。InputStreamReader
:将字节输入流转换为字符输入流。FilterReader
:为其他字符输入流提供附加功能。LineNumberReader
:能够跟踪行号的输入流。
字符输出流(Writer)
FileWriter
:将字符写入文件。CharArrayWriter
:将字符写入字符数组。StringWriter
:将字符写入字符串缓冲区。BufferedWriter
:为字符输出流提供缓冲功能,提高写入效率。OutputStreamWriter
:将字符输出流转换为字节输出流。FilterWriter
:为其他字符输出流提供附加功能。PrintWriter
:提供便捷的方法写入格式化文本。
综合示例
以下是一个简单的示例,展示了如何使用字节流和字符流进行文件读写操作。
字节流示例
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
字符流示例
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CharStreamExample {
public static void main(String[] args) {
try (FileReader fr = new FileReader("input.txt");
FileWriter fw = new FileWriter("output.txt")) {
int data;
while ((data = fr.read()) != -1) {
fw.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
总结
Java 中的 IO 流分为字节流和字符流两大类。字节流用于处理二进制数据,字符流用于处理文本数据。每种流又分为输入流和输出流。通过这些流,Java 程序可以灵活地处理各种形式的输入和输出操作。
BIO、NIO、AIO 有什么区别?
BIO:Block IO 同步阻塞式 IO
NIO:Non IO 同步非阻塞 IO
AIO:Asynchronous IO 异步非阻塞IO
BIO是一个连接一个线程。JDK4之前的唯一选择
NIO是一个请求一个线程。JDK4之后开始支持,常见聊天服务器
AIO是一个有效请求一个线程。JDK7之后开始支持,常见相册服务器
BIO(Blocking I/O)、NIO(Non-blocking I/O)、AIO(Asynchronous I/O)是 Java 中不同的 I/O 模型,它们在处理数据流和通信方面有显著的区别。以下是对这三种 I/O 模型的详细解释及其区别:
1. BIO(Blocking I/O)
特点
- 阻塞模式:线程在进行 I/O 操作时会被阻塞,直到操作完成。
- 一个线程对应一个连接:每个客户端请求都会分配一个线程来处理,当连接数增加时,需要更多的线程来处理,导致资源消耗增加。
- 简单易用:编程模型相对简单,易于理解和实现。
适用场景
适用于连接数少且服务端资源较充裕的场景,例如:传统的同步阻塞式通信模型。
示例
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();
new Thread(() -> {
try {
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
2. NIO(Non-blocking I/O)
特点
- 非阻塞模式:线程在进行 I/O 操作时不会被阻塞,可以在等待数据的同时进行其他操作。
- 多路复用:通过
Selector
实现,一个线程可以管理多个通道(Channel),减少线程数量。 - 缓冲区:数据读写通过缓冲区(Buffer)进行,提高了数据处理效率。
适用场景
适用于连接数多且连接时间较长的场景,例如:高并发的网络服务器。
示例
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, bytesRead));
}
}
}
}
}
}
3. AIO(Asynchronous I/O)
特点
- 异步非阻塞模式:操作结果通过回调机制通知调用者,线程无需等待操作完成。
- 简化并发处理:通过异步机制和回调函数,减少了线程数量和同步开销,提高了并发处理能力。
- 更高效:适用于 I/O 密集型应用,能够更好地利用系统资源。
适用场景
适用于高延迟和高吞吐量的场景,例如:大规模分布式系统、实时通信系统。
示例
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
public class AIOServer {
public static void main(String[] args) throws IOException, InterruptedException {
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel result, Object attachment) {
serverSocketChannel.accept(null, this); // 继续接收下一个连接
ByteBuffer buffer = ByteBuffer.allocate(1024);
result.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer attachment) {
if (bytesRead > 0) {
attachment.flip();
System.out.println(new String(attachment.array(), 0, bytesRead));
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
// 阻塞主线程
Thread.currentThread().join();
}
}
总结
- BIO(Blocking I/O):适用于简单的应用程序或小规模系统,编程简单但性能较低。
- NIO(Non-blocking I/O):适用于高并发、高吞吐量的网络应用,通过多路复用和非阻塞 I/O 提高性能和资源利用率。
- AIO(Asynchronous I/O):适用于高延迟、高并发的场景,通过异步 I/O 和回调机制进一步提高性能和并发处理能力。
选择适合的 I/O 模型取决于具体的应用场景和需求。
Files的常用方法都有哪些?
Files
是 Java NIO 提供的一个实用类,包含了许多静态方法来进行文件和目录的操作。以下是一些常用的 Files
方法及其用途:
创建文件和目录
-
创建文件
Path path = Paths.get("example.txt"); Files.createFile(path);
-
创建目录
Path path = Paths.get("exampleDir"); Files.createDirectory(path);
-
创建多级目录
Path path = Paths.get("exampleDir/subDir"); Files.createDirectories(path);
删除文件和目录
-
删除文件或空目录
java复制代码
Path path = Paths.get("example.txt"); Files.delete(path);
-
删除文件或目录(如果存在)
Path path = Paths.get("example.txt"); Files.deleteIfExists(path);
-
下面是一个综合示例,展示了如何使用
Files
类的各种方法:import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; import java.util.List; import java.util.Set; public class FilesExample { public static void main(String[] args) throws IOException { Path path = Paths.get("example.txt"); // 创建文件 if (!Files.exists(path)) { Files.createFile(path); } // 写入文件 List<String> lines = Arrays.asList("Hello", "World"); Files.write(path, lines, StandardCharsets.UTF_8); // 读取文件 List<String> readLines = Files.readAllLines(path, StandardCharsets.UTF_8); readLines.forEach(System.out::println); // 复制文件 Path targetPath = Paths.get("example_copy.txt"); Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING); // 移动文件 Path movedPath = Paths.get("example_moved.txt"); Files.move(targetPath, movedPath, StandardCopyOption.REPLACE_EXISTING); // 获取文件大小 long size = Files.size(path); System.out.println("File size: " + size); // 删除文件 Files.delete(movedPath); Files.delete(path); } }
以上列出了
Files
类的一些常用方法,这些方法大大简化了文件和目录的操作,使代码更加简洁和易于维护。 -
检查文件是否存在
Path path = Paths.get("example.txt"); boolean exists = Files.exists(path);
-
检查文件是否为目录
Path path = Paths.get("exampleDir"); boolean isDirectory = Files.isDirectory(path);
-
检查文件是否为常规文件
Path path = Paths.get("example.txt"); boolean isRegularFile = Files.isRegularFile(path);
-
检查文件是否可读、可写、可执行
Path path = Paths.get("example.txt"); boolean isReadable = Files.isReadable(path); boolean isWritable = Files.isWritable(path); boolean isExecutable = Files.isExecutable(path);
-
文件和目录流
-
列出目录内容
Path path = Paths.get("exampleDir"); try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { for (Path entry : stream) { System.out.println(entry.getFileName()); } }
-
递归遍历目录
Path start = Paths.get("exampleDir"); Files.walkFileTree(start, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { System.out.println(file.getFileName()); return FileVisitResult.CONTINUE; } });
-
文件属性
-
获取文件大小
Path path = Paths.get("example.txt"); long size = Files.size(path);
-
获取文件属性
Path path = Paths.get("example.txt"); BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
-
设置文件权限
Path path = Paths.get("example.txt"); Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-r--r--"); Files.setPosixFilePermissions(path, perms);
-
读取和写入文件
-
读取文件到字节数组
Path path = Paths.get("example.txt"); byte[] bytes = Files.readAllBytes(path);
-
读取文件到字符串列表
Path path = Paths.get("example.txt"); List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
-
写入字节数组到文件
Path path = Paths.get("example.txt"); byte[] bytes = "Hello, World!".getBytes(StandardCharsets.UTF_8); Files.write(path, bytes);
-
写入字符串列表到文件
Path path = Paths.get("example.txt"); List<String> lines = Arrays.asList("Hello", "World"); Files.write(path, lines, StandardCharsets.UTF_8);
-
复制和移动文件
-
复制文件
Path source = Paths.get("source.txt"); Path target = Paths.get("target.txt"); Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
-
移动文件
Path source = Paths.get("source.txt"); Path target = Paths.get("target.txt"); Files.move(source, target, StandardCopyOption.REPLACE_EXISTING)
file.getName();
file.getPath();
file.delete();
file.exits();
file.createDirectory();
file.copy();
file.move();
file.size();
file.read();
file.write();
62.如何实现对象克隆?
1、实现Coloneable接口并重写Object类中的clone()方法;//浅克隆
2、实现Serializable [ˈsɪərɪəlaɪzəbl]接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
哪些集合类是线程安全的?
Vector:相比ArrayList多了线程安全;
HashTable:相比HashMap多了线程安全;
ConcurrentHashMap:高效且线程安全;
Stack:继承于Vector,也是线程安全
迭代器 Iterator 是什么?
迭代器是一种设计模式,它是一个对象,可以遍历并选择序列中的对象。
15.Iterator 怎么使用?有什么特点?
next();
hashNext();
remove();
Iterator接口被Collection接口继承,Collection接口的iterator()方法返回一个iterator对象。
Iterator 和 ListIterator 有什么区别?
ListIterator有add()方法,可以向List中添加对象,而Iterator不能;
ListIterator有hasPrevious()和previous()方法,可以向前遍历,Iterator不能;
ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现;
ListIterator可以实现对对象的修改,使用set()实现;而Iterator只能遍历,不能修改
怎么确保一个集合不能被修改?
可以使用Collections.unmodifiableCollection(Collection c) 方法创建一个只读集合
这样改变集合的任何操作都会抛出Java. lang. UnsupportedOperationException异常。
.进程、线程、程序
1. 程序、进程和线程的区分: 程序(program):为完成特定任务,用某种语言编写的`一组指令的集合`。即指一段静态的代码。 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。程序是静态的,进程是动态的。 进程作为操作系统调度和分配资源的最小单位。 线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。 线程作为CPU调度和执行的最小单位 2. 线程调度策略 分时调度:所有线程`轮流使用` CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。 抢占式调度:让`优先级高`的线程以`较大的概率`优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
在计算机科学中,进程(Process)、线程(Thread)和程序(Program)是三个重要的概念,它们在操作系统和应用程序的运行中起着关键作用。以下是对它们的详细解释及区别:
程序(Program)
定义:程序是存储在磁盘上的一组指令的集合,是一个静态的实体。它是按特定顺序组织的一系列指令的集合,用于完成特定任务。
特点:
- 静态:程序是一个静态代码,它存储在硬盘等存储设备上。
- 文件:程序通常是一个文件,包含了代码和需要的数据。
- 不运行:程序本身不占用系统资源,如CPU、内存等,只有当它被加载到内存中执行时,才会占用系统资源。
示例:存储在硬盘上的一个Java程序文件 HelloWorld.java
。
进程(Process)
定义:进程是程序在计算机中的一次运行实例。它是一个动态的实体,是程序执行的过程,包括了程序计数器、堆栈和数据段等。
特点:
- 动态:进程是程序的执行实例,是一个动态实体。
- 占用资源:进程在运行时会占用系统资源,如CPU时间、内存空间、文件句柄等。
- 独立:每个进程在系统中是独立的,拥有自己的内存空间和资源。
- 生命周期:进程有自己的生命周期,包括创建、运行、等待、终止等状态。
示例:运行中的Java程序 java HelloWorld
就是一个进程。
线程(Thread)
定义:线程是进程中的一个独立执行路径,是CPU调度的基本单位。一个进程可以包含多个线程,它们共享进程的资源但可以独立执行。
特点:
- 轻量级:线程是比进程更轻量级的执行单元,创建和销毁的开销比进程小。
- 共享资源:同一进程中的线程共享进程的资源,如内存、文件句柄等。
- 独立执行:每个线程都有自己的程序计数器、栈和局部变量,但共享进程的堆和方法区。
- 并发执行:在多核处理器上,多个线程可以并发执行,从而提高程序的执行效率。
示例:一个Java程序中,可以创建多个线程来同时执行不同的任务,例如使用 Thread
类或实现 Runnable
接口来创建线程。
关系与区别
-
程序与进程:
- 程序是静态的代码,而进程是程序的动态执行实例。
- 一个程序可以多次执行,每次执行都是一个独立的进程。
-
进程与线程:
- 进程是资源分配的单位,而线程是CPU调度的单位。
- 进程之间是独立的,而同一进程内的线程是共享资源的。
- 进程的切换开销大,线程的切换开销小。
-
程序与线程:
- 程序是静态的指令集,线程是程序运行时的执行路径。
- 线程属于进程,一个进程可以包含多个线程来执行不同的任务。
示例代码
以下是一个简单的Java程序,展示了如何创建和使用线程:
public class ThreadExample {
public static void main(String[] args) {
// 创建一个新的线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from the new thread!");
}
});
// 启动线程
thread.start();
// 主线程的输出
System.out.println("Hello from the main thread!");
}
}
11.创建线程的基本方法
在Java中,创建线程的基本方法有两种:继承 Thread
类和实现 Runnable
接口。下面分别介绍这两种方法,以及如何使用它们创建和启动线程。
1. 继承 Thread
类
通过继承 Thread
类,并重写其 run
方法,可以创建一个新的线程类。然后实例化这个类并调用其 start
方法启动线程。
示例代码:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello from MyThread!");
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程,run 方法将被调用
}
}
解释:
- 创建了一个继承
Thread
类的MyThread
类,并重写了run
方法。 - 在
main
方法中,实例化MyThread
对象,并调用start
方法启动线程。
2. 实现 Runnable
接口
通过实现 Runnable
接口,并将该接口的实现传递给 Thread
对象的构造函数,可以创建一个新的线程。然后调用 Thread
对象的 start
方法启动线程。
示例代码:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello from MyRunnable!");
}
}
public class ThreadExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程,run 方法将被调用
}
}
解释:
- 创建了一个实现
Runnable
接口的MyRunnable
类,并实现了run
方法。 - 在
main
方法中,实例化MyRunnable
对象,并将其传递给Thread
对象的构造函数,然后调用start
方法启动线程。
3.匿名内部类实现
可以使用匿名内部类来创建线程。这种方式简洁,适合只需使用一次的场景。
public class ThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from anonymous Runnable!");
}
});
thread.start(); // 启动线程,run 方法将被调用
}
}
4.Lambda表达式(Java 8及以上)
使用Lambda表达式可以进一步简化实现 Runnable
接口的代码。
示例代码:
public class ThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Hello from Lambda Runnable!"));
thread.start(); // 启动线程,run 方法将被调用
}
}
总结
- 继承
Thread
类:适用于需要扩展Thread
类并重写run
方法的场景。 - 实现
Runnable
接口:适用于需要实现Runnable
接口并将其传递给Thread
对象的场景。这种方式更灵活,因为一个类可以实现多个接口,而Java不支持多重继承。 - 匿名内部类和Lambda表达式:简化了代码,实现一次性任务的场景非常方便。
这些方法可以帮助你创建和管理线程,以实现并发编程,充分利用多核处理器的性能。
什么是线程?为什么要使用线程?
线程是操作系统能够进行运算调度的最小单元,它被包含在进程中,是进程中的实际运作单位。使用线程可以实现并发执行,提升应用程序的性能,尤其是在I/O密集型和计算密集型任务中。
什么是线程的生命周期?
答案: 线程的生命周期包括以下几个状态:
- 新建(New):线程对象被创建。
- 就绪(Runnable):线程已准备好运行,等待CPU调度。
- 运行(Running):线程正在执行。
- 阻塞(Blocked):线程阻塞,等待某种条件。
- 死亡(Terminated):线程执行完毕或因异常终止。
什么是线程同步?如何实现线程同步?
答案: 线程同步是为了确保多个线程在访问共享资源时不会产生冲突或数据不一致。可以通过以下方式实现:
synchronized
关键字:用于方法或代码块。Lock
接口及其实现类(如ReentrantLock
)。volatile
关键字:确保变量的可见性。- 原子类(如
AtomicInteger
):用于一些简单的同步场景。
什么是死锁?如何避免死锁?
答案: 死锁是指两个或多个线程互相等待对方释放资源,从而陷入无限等待的状态。避免死锁的方法包括:
- 避免嵌套锁定。
- 使用锁时按照相同顺序加锁。
- 使用超时机制。
- 使用死锁检测机制。
什么是守护线程(Daemon Thread)?如何创建守护线程?
答案:守护线程是一种特殊的线程,在JVM中所有非守护线程都结束时,JVM会退出,守护线程也会随之终止。守护线程通常用于执行后台任务,如垃圾回收器。
创建守护线程:
Thread daemonThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("Daemon thread running...");
}
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
什么是线程优先级?它在Java中如何工作?
答案:线程优先级是用于指示线程调度的一个标志,优先级较高的线程会获得更多的CPU时间片。Java中的线程优先级范围为1(最低)到10(最高),默认值为5。
设置线程优先级:
Thread highPriorityThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("High priority thread running...");
}
});
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
highPriorityThread.start();
什么是线程调度?Java中的线程调度策略是什么?
答案:线程调度是操作系统根据线程优先级和状态,将CPU时间片分配给各个线程的过程。Java中的线程调度是抢占式的,即优先级高的线程优先获得CPU时间片。此外,Java也支持时间片轮转调度。
什么是上下文切换?为什么它在多线程编程中重要?
答案:上下文切换是指CPU从一个线程切换到另一个线程时,需要保存当前线程的状态,并恢复新线程的状态。上下文切换是多线程并发执行的基础,但频繁的上下文切换会带来性能开销。
什么是线程安全?如何实现线程安全?
答案:线程安全是指在多线程环境中,多个线程同时访问共享资源时,不会出现数据不一致或数据污染的情况。实现线程安全的方法包括:
- 使用同步(
synchronized
关键字)。 - 使用显式锁(如
ReentrantLock
)。 - 使用线程安全的数据结构(如
ConcurrentHashMap
)。 - 使用
volatile
关键字确保变量的可见性。 - 使用原子类(如
AtomicInteger
)。
什么是 synchronized
关键字?它的作用是什么?
答案:synchronized
关键字用于实现线程同步,确保同一时刻只有一个线程可以访问同步代码块或方法。它可以用来修饰方法或代码块,确保共享资源的互斥访问。
示例代码:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
Thread t1 = new Thread(example::increment);
Thread t2 = new Thread(example::increment);
t1.start();
t2.start();
}
}
什么是 volatile
关键字?它的作用是什么?
答案:volatile
关键字用于声明一个变量在多个线程间可见,确保变量的修改对所有线程立即可见。它防止线程对变量的缓存,从而避免读取到过时的值。
示例代码:
public class VolatileExample {
private volatile boolean running = true;
public void run() {
while (running) {
// 执行任务
}
}
public void stop() {
running = false;
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
Thread thread = new Thread(example::run);
thread.start();
// 其他线程调用 stop() 方法停止运行
example.stop();
}
}
解释一下 Java 中的锁机制。
答案:Java中的锁机制用于控制多个线程对共享资源的访问,确保线程安全。常见的锁机制包括:
- 内置锁(synchronized):用于同步方法和代码块。
- 显式锁(ReentrantLock):提供了比synchronized更灵活的锁机制,如可重入锁、公平锁等。
什么是 ReentrantLock
?它与 synchronized
的区别是什么?
答案:ReentrantLock
是 Lock
接口的一个实现类,它提供了与内置锁(synchronized)类似的功能,但具有更多的灵活性和功能,如:
- 可重入性:同一个线程可以多次获得锁。
- 公平性:可以选择公平锁或非公平锁。
- 可中断性:可以中断正在等待锁的线程。
示例代码:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Thread t1 = new Thread(example::increment);
Thread t2 = new Thread(example::increment);
t1.start();
t2.start();
}
}
重入锁(ReentrantLock)
重入锁是指同一个线程可以多次获取同一把锁而不会被阻塞。这意味着一个线程可以在持有锁的情况下再次获取锁。
特点
- 可重入性:一个线程在持有锁的情况下,可以再次获取该锁而不会被阻塞。这使得在实现递归方法和跨方法调用时更加方便。
- 提供更多锁定机制:相比于
synchronized
关键字,ReentrantLock
提供了更多的功能,如等待可中断、实现公平锁、绑定多个条件等。
示例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock in outer method");
inner(); // 调用内部方法
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock in inner method");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
new Thread(example::outer).start();
new Thread(example::outer).start();
}
}
在这个例子中,同一个线程在调用 outer
方法时可以重新获取锁并进入 inner
方法。
公平锁(Fair Lock)
公平锁是指多个线程按请求锁的顺序来获取锁,遵循先来先服务的原则。这种锁可以防止线程饥饿,确保每个请求锁的线程最终都能获得锁。
特点
- 公平性:保证线程按照请求锁的顺序获取锁,避免某些线程长时间得不到锁的情况。
- 可能带来性能开销:因为公平锁需要维护一个有序的等待队列,所以可能会带来一些性能开销。
实现方式
在 Java 中,通过 ReentrantLock
的构造函数可以实现公平锁。
示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
private final Lock lock = new ReentrantLock(true); // 创建公平锁
public void accessResource() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock");
// 模拟执行任务
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
FairLockExample example = new FairLockExample();
Runnable task = example::accessResource;
for (int i = 0; i < 5; i++) {
new Thread(task).start();
}
}
}
在这个例子中,ReentrantLock(true)
创建了一个公平锁,保证多个线程按顺序获取锁。
总结
- 重入锁(ReentrantLock):允许线程多次获取同一把锁,提供了更多的锁定机制,如等待可中断、实现公平锁、绑定多个条件等。适用于需要高级功能的场景。
- 公平锁(Fair Lock):保证锁的获取顺序是按请求顺序,避免线程饥饿。适用于需要严格控制锁获取顺序的场景。
这两种锁在多线程编程中提供了灵活性和更强的控制力,可以根据实际需求选择适合的锁类型。
什么是死锁?如何避免死锁?
答案:死锁是指两个或多个线程互相等待对方释放资源,从而陷入无限等待的状态。避免死锁的方法包括:
- 避免嵌套锁定:尽量减少持有多个锁的情况。
- 使用锁时按照相同顺序加锁。
- 使用超时机制:设置锁超时时间,避免无限等待。
- 使用死锁检测机制:检测死锁并采取措施。
什么是乐观锁和悲观锁?它们的区别是什么?
答案:
- 乐观锁:假设不会发生冲突,在操作数据时不加锁,而是在提交更新时检查冲突。如
CAS
(Compare-And-Swap)机制。 - 悲观锁:假设不会发生冲突,在操作数据时对数据加锁,以防止并发冲突。悲观锁通常在数据库事务中使用,确保数据一致性。
线程间通信
1.什么是线程间通
信?Java 中实现线程间通信的方式有哪些?
答案:线程间通信是指多个线程之间交换数据或协调操作的过程。在Java中,线程间通信的常用方式包括:
- 使用
wait()
、notify()
和notifyAll()
方法。 - 使用
BlockingQueue
等并发集合类。 - 使用高级同步工具类(如
CountDownLatch
、CyclicBarrier
和Semaphore
)。 - 使用
PipedInputStream
和PipedOutputStream
实现线程间的字节流通信。
解释一下 wait
、notify
和 notifyAll
的工作机制。
答案:
wait()
:使当前线程等待,直到其他线程调用notify()
或notifyAll()
方法。调用wait()
方法的线程必须持有对象的监视器。notify()
:唤醒在此对象监视器上等待的某个线程。如果有多个线程在等待,则随机选择一个。notifyAll()
:唤醒在此对象监视器上等待的所有线程。
示例代码:
public class WaitNotifyExample {
private final Object lock = new Object();
public void doWait() {
synchronized (lock) {
try {
System.out.println("Thread waiting...");
lock.wait();
System.out.println("Thread resumed.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public void doNotify() {
synchronized (lock) {
System.out.println("Thread notifying...");
lock.notify();
}
}
public static void main(String[] args) {
WaitNotifyExample example = new WaitNotifyExample();
Thread t1 = new Thread(example::doWait);
Thread t2 = new Thread(example::doNotify);
t1.start();
try {
Thread.sleep(1000); // 确保 t1 先执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
t2.start();
}
}
什么是 CountDownLatch
?如何使用它?
答案:CountDownLatch
是一个同步辅助类,用于使一个或多个线程等待,直到其他线程完成一组操作。通过一个计数器实现,计数器初始化为一个计数值,每调用一次 countDown()
方法,计数器减一,当计数器到达零时,所有等待的线程被唤醒。
示例代码:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
private static final int THREAD_COUNT = 3;
private static final CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " is working...");
latch.countDown();
}).start();
}
try {
latch.await(); // 等待所有线程完成
System.out.println("All threads have finished.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
什么是 CyclicBarrier
?如何使用它?
答案:CyclicBarrier
是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。CyclicBarrier
可以重用,线程到达屏障点后可以继续下一个周期。
示例代码:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
private static final int THREAD_COUNT = 3;
private static final CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT,
() -> System.out.println("All threads reached the barrier."));
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " is working...");
try {
barrier.await();
System.out.println(Thread.currentThread().getName() + " has crossed the barrier.");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
什么是 Semaphore
?它在多线程编程中如何使用?
Semaphore
是一个计数信号量,常用于控制多个线程对共享资源的访问。Semaphore
通过一个许可计数器控制访问,线程可以通过 acquire()
方法获取许可,通过 release()
方法释放许可。
示例代码:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static final int PERMITS = 2;
private static final Semaphore semaphore = new Semaphore(PERMITS);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " acquired permit.");
Thread.sleep(1000); // 模拟任务执行
System.out.println(Thread.currentThread().getName() + " released permit.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
}).start();
}
}
}
什么是 Exchanger
?它的用途是什么?
答案:Exchanger
是一个同步点,在这个同步点,两个线程可以交换彼此的数据。它用于在两个线程之间进行数据交换和传递。
示例代码:
import java.util.concurrent.Exchanger;
public class ExchangerExample {
private static final Exchanger<String> exchanger = new Exchanger<>();
public static void main(String[] args) {
new Thread(() -> {
try {
String data = "Data from Thread A";
System.out.println("Thread A is exchanging data...");
String receivedData = exchanger.exchange(data);
System.out.println("Thread A received: " + receivedData);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
new Thread(() -> {
try {
String data = "Data from Thread B";
System.out.println("Thread B is exchanging data...");
String receivedData = exchanger.exchange(data);
System.out.println("Thread B received: " + receivedData);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
线程池
.
-
什么是线程池?为什么要使用线程池?
答案:线程池是一组预先创建的线程,线程池管理这些线程的生命周期。使用线程池的原因包括:
- 提高性能:通过重用现有线程,减少线程创建和销毁的开销。
- 资源控制:限制线程的数量,避免系统过载。
- 简化并发编程:提供了便捷的任务提交和执行方式。
如何创建一个线程池?请写出示例代码。
答案:可以通过 Executors
工具类或直接使用 ThreadPoolExecutor
来创建线程池。
使用 Executors
([ɪgˈzɛkjʊtəz])工具类:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " is running...");
});
}
executor.shutdown();
}
}
使用 ThreadPoolExecutor
来创建线程池
示例代码
import java.util.concurrent.*;
public class CustomThreadPoolExecutorExample {
public static void main(String[] args) {
// 核心线程数
int corePoolSize = 2;
// 最大线程数
int maximumPoolSize = 4;
// 线程空闲时间
long keepAliveTime = 10;
// 时间单位
TimeUnit unit = TimeUnit.SECONDS;
// 任务队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
// 线程工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 提交任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " is running on " + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task " + taskId + " completed.");
});
}
// 关闭线程池
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
说明
-
核心线程数和最大线程数:
- 当任务提交时,如果线程池中的线程数量小于核心线程数,会创建新的线程来处理任务。
- 如果线程池中的线程数量达到核心线程数,任务会被放入任务队列。
- 如果任务队列已满且线程池中的线程数量小于最大线程数,会创建新的线程来处理任务。
- 如果任务队列已满且线程池中的线程数量达到最大线程数,会执行拒绝策略。
-
任务队列:
- 这里使用了
LinkedBlockingQueue
,可以指定队列的容量。 - 任务队列用于保存等待执行的任务。
- 这里使用了
-
线程工厂:
- 用于创建新线程,默认使用
Executors.defaultThreadFactory()
。
- 用于创建新线程,默认使用
-
拒绝策略:
- 当任务无法提交到线程池时执行。常见的策略包括:
AbortPolicy
:直接抛出RejectedExecutionException
。CallerRunsPolicy
:由提交任务的线程执行该任务。DiscardPolicy
:丢弃任务,不抛异常。DiscardOldestPolicy
:丢弃最早的未处理任务并重新尝试提交。
- 当任务无法提交到线程池时执行。常见的策略包括:
总结
通过 ThreadPoolExecutor
可以灵活配置线程池的行为,适应不同的应用场景。上述示例展示了如何自定义线程池的核心参数、任务队列和拒绝策略,以满足特定的需求。
-
解释一下
ThreadPoolExecutor
的工作原理。
答案:ThreadPoolExecutor
是 Executor
接口的实现类,提供了一个灵活的线程池实现。其主要工作原理包括:
- 核心线程数(corePoolSize):线程池在空闲时保持的最小线程数量。
- 最大线程数(maximumPoolSize):线程池能创建的最大线程数量。
- 任务队列(workQueue):用于保存等待执行的任务。
- 线程工厂(ThreadFactory):用于创建新线程。
- 拒绝策略(RejectedExecutionHandler):当任务无法提交到线程池时的处理策略。
当任务提交到线程池时:
-
如果线程池中的线程数量小于核心线程数,创建一个新线程执行任务。
-
如果线程池中的线程数量达到核心线程数,将任务加入任务队列。
-
如果任务队列已满且线程池中的线程数量小于最大线程数,创建新线程执行任务。
-
如果任务队列已满且线程池中的线程数量达到最大线程数,执行拒绝策略。
-
什么是
Executors
工具类?它提供了哪些常用的线程池实现?
答案:Executors
是一个工具类,提供了一些便捷的方法来创建和管理线程池。常用的线程池实现包括:
newFixedThreadPool(int nThreads)
:创建固定大小的线程池。newCachedThreadPool()
:创建一个根据需要创建新线程的线程池,空闲线程会被回收。newSingleThreadExecutor()
:创建一个只有一个线程的线程池。newScheduledThreadPool(int corePoolSize)
:创建一个支持定时和周期性任务执行的线程池。
-
如何正确关闭线程池?
答案:关闭线程池的方法包括:
shutdown()
:启动有序关闭,不再接受新任务,等待已提交任务执行完毕。shutdownNow()
:试图停止所有正在执行的任务,并返回等待执行的任务列表。
示例代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ShutdownThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " is running...");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown(); // 启动有序关闭
try {
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 强制关闭
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
说一下 runnable 和 callable 有什么区别?
实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程并不能返回执行结果
Callable接口的call方法允许抛出异常,而Runnable接口的run方法的异常只能在内部消化,不能继续上抛
线程池中 submit()和 execute()方法有什么区别?
execute() 参数 Runnable ;submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable)
execute() 没有返回值;而 submit() 有返回值
submit()的返回值Future调用get方法时,可以捕获处理异常
在 java 程序中怎么保证多线程的运行安全?
JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
synchronized、volatile、LOCK,可以解决可见性问题
Happens-Before 规则可以解决有序性问题
多线程锁的升级原理是什么?
锁的级别:无锁 => 偏向锁 => 轻量级锁 => 重量级锁
无锁:没有对资源进行锁定,所有线程都可以访问,但是只有一个能修改成功,其他的线程会不断尝试,直至修改成功。
偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,偏向锁,指的就是偏向第一个加锁线程,该线程不会主动释放偏向锁,只有当其他线程尝试竞争偏向锁时才会被释放。
偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;
如果线程处于活动状态,升级为轻量级锁的状态。
轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
ThreadLocal 是什么?有哪些使用场景?
ThreadLocal是线程本地存储,在每个线程中都创建了一个ThreadLocalMap对象,每个线程可以访问自己内部ThreadLocal对象内的value。
经典的使用场景是为每个线程分配一个JDBC连接的Connection,这样就可以保证每个线程都在各自的Connection上进行数据库的操作,不会出现A线程关了B线程的Connection,还有Session管理等问题。
说一下 synchronized 底层实现原理?
同步代码块是通过monitorenter和monitorexit指令获取线程的执行权;
同步方法是通过加ACC_SYNCHRONIZED 标识实现线程的执行权的控制
19.synchronized 和 volatile 的区别是什么?
volatile本质是在告诉vm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronize则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。
volatile仅能实现变量的修改可见性,不能保证原子性;synchronize可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞;synchronize可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化,synchronize标记的变量可以被编译器优化
synchronized 和 Lock 有什么区别?
synchronized是关键字,属于jvm层面;Lock是具体类,是api层面的锁;
synchronized无法获取锁的状态,Lock可以判断;
synchronized用于少量同步,Lock用于大量同步。
synchronized 和 ReentrantLock 区别是什么?
synchronized代码执行结束后线程自动释放对锁的占用;Reentrantlock需要手动释放锁;
synchronized不可中断,除非抛出异常或者执行完成;Reentrantlock可中断;
synchronize非公平锁;Reentrantlock默认非公平锁,也可公平锁;
ReentrantLock用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized要么随机唤醒一个,要么唤醒全部线程。
说一下 atomic 的原理?
作用:多线程下将属性设置为atomic可以保证读取数据的一致性。
CAS(Compare And Swap),乐观锁的机制,先比较再交换,以实现原子性
什么是反射?
允许程序在运行时检查和操作类,接口,字段,方法等结构。提供了动态访问和操作代码结果的能力。可以修改任意修饰词的方法,成员变量(包括private,protect)
什么是 java 序列化?什么情况下需要序列化?
序列化:将java对象转换成字节流的过程。
反序列化:将字节流转换成java对象的过程。
当java对象需要在网络上传输或者持久化存储到文件中时,就需要对Java对象进行序列化处理。
序列化的实现:类实现Serializable接口。
动态代理是什么?有哪些应用?
在运行时,创建一个新的类,即创建动态代理,可以调用和扩展目标类的方法。动态代理的类是自动生成的。
应用:Spring的AOP,加事务,加权限,加日志
怎么实现动态代理?
基于jdk,需要实现InvocationHandler接口,重写invoke方法。
基于cglib,需要jar包依赖;
基于javassist
动态代理和静态代理
在 Java 中,代理模式用于在不修改原有类的情况下,通过代理对象对目标对象进行扩展功能。代理分为静态代理和动态代理两种方式。下面我们详细介绍这两种代理模式。
静态代理
静态代理在编译时就已经确定代理类,代理类和目标类实现相同的接口,并且代理类中包含对目标对象的引用。
优点
- 可以在不修改目标对象的情况下对其进行扩展。
- 可以通过代理类对目标对象进行控制。
缺点
- 代理类需要实现与目标对象相同的接口,增加了代码量。
- 如果接口发生变化,代理类和目标类都需要修改。
示例
首先定义一个接口和目标类:
// 定义接口
public interface Service {
void perform();
}
// 目标类
public class RealService implements Service {
@Override
public void perform() {
System.out.println("RealService is performing...");
}
}
然后定义代理类:
// 代理类
public class StaticProxy implements Service {
private RealService realService;
public StaticProxy(RealService realService) {
this.realService = realService;
}
@Override
public void perform() {
System.out.println("StaticProxy: Before performing...");
realService.perform();
System.out.println("StaticProxy: After performing...");
}
}
使用静态代理
public class Main {
public static void main(String[] args) {
RealService realService = new RealService();
StaticProxy proxy = new StaticProxy(realService);
proxy.perform();
}
}
输出结果:
动态代理
动态代理是在运行时创建代理类,而不是在编译时确定。Java 提供了 java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口来实现动态代理。
优点
- 代理类在运行时生成,无需提前编写代理类,减少了代码量。
- 更加灵活,能够代理任意接口。
缺点
- 由于使用反射机制,性能略低于静态代理。
- 调试较困难,代码可读性较差。
示例
首先定义一个接口和目标类(与静态代理相同):
// 定义接口
public interface Service {
void perform();
}
// 目标类
public class RealService implements Service {
@Override
public void perform() {
System.out.println("RealService is performing...");
}
}
然后定义动态代理处理器:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxyHandler implements InvocationHandler {
private Object target;
public DynamicProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("DynamicProxy: Before performing...");
Object result = method.invoke(target, args);
System.out.println("DynamicProxy: After performing...");
return result;
}
public static void main(String[] args) {
RealService realService = new RealService();
Service proxyInstance = (Service) Proxy.newProxyInstance(
realService.getClass().getClassLoader(),
realService.getClass().getInterfaces(),
new DynamicProxyHandler(realService)
);
proxyInstance.perform();
}
}
输出结果:
DynamicProxy: Before performing...
RealService is performing...
DynamicProxy: After performing...
对比
- 实现方式:
- 静态代理:在编译期生成代理类。
- 动态代理:在运行时生成代理类。
- 灵活性:
- 静态代理:需要手动编写代理类,不够灵活。
- 动态代理:通过反射机制,可以代理任意接口,更加灵活。
- 代码量:
- 静态代理:需要编写大量代理类,代码量大。
- 动态代理:减少了代理类的编写,代码量小。
- 性能:
- 静态代理:性能较好。
- 动态代理:由于使用了反射机制,性能略低。
通过静态代理和动态代理,可以在不修改原有类的情况下,对其进行扩展和控制。在实际开发中,根据具体需求选择合适的代理方式。
为什么要使用克隆?
如果直接使用=给对象赋值的话,那么两个对象其实指向的是同一个地址,其中一个值改变时,另一个也会随之改变。
62.如何实现对象克隆?
1、实现Coloneable接口并重写Object类中的clone()方法;
2、实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
63.深克隆和浅克隆区别是什么?
深克隆:新旧对象不共享一个地址;
浅克隆:新旧对象共享一个地址,改变一个,另一个也会改变
数据库的三范式是什么?
第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项
第二范式:要求实体的属性完全依赖于主关键字,所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。
第三范式:任何非主属性不依赖于其他非主属性
2.一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 mysql 数据库,又插入了一条数据,此时 id 是几?
表类型如果是MyISAM,那ID就是8
表类型如果是InnoDB,那ID就是6
3.如何获取当前数据库版本?
select version()
4.说一下 ACID 是什么?
Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
5.char 和 varchar 的区别是什么?
char(n) :固定长度类型。优点:效率高;缺点:占用空间;
varchar(n) :可变长度,存储的值是每个值占用的字节再加上一个用来记录其长度的字节的长度。
所以,从空间上考虑 varcahr 比较合适;从效率上考虑 char 比较合适,二者使用需要权衡。
6.float 和 double 的区别是什么?
float 最多可以存储 8 位的十进制数,并在内存中占 4 字节。
double 最可可以存储 16 位的十进制数,并在内存中占 8 字节。
7.mysql 的内连接、左连接、右连接有什么区别?
内连接是把匹配的关联数据显示出来;
左连接是左边的表全部显示出来,右边的表显示出符合条件的数据,如果没有就显示为NULL
右连接正好相反。
8.mysql 索引是怎么实现的?
索引是满足某种特定查找算法的数据结构,而这些数据结构会以某种方式指向数据,从而实现高效查找数据。
目前主流的数据库引擎的索引都是 B+ 树实现的,B+ 树的搜索效率,可以到达二分法的性能,找到数据区域之后就找到了完整的数据结构了,所有索引的性能也是更好的。
9.怎么验证 mysql 的索引是否满足需求?
explain select * from table where column=''
使用 explain 查看 SQL 是如何执行查询语句的,从而分析你的索引是否满足需求。
说一下数据库的事务隔离
数据库的事务隔离是确保多个事务并发执行时,事务之间的数据操作不互相干扰,保证数据一致性的一种机制。事务隔离级别决定了一个事务在多大程度上可以看到其他事务的中间状态。SQL标准定义了四种事务隔离级别,分别为:
- 未提交读(Read Uncommitted)
- 提交读(Read Committed)
- 可重复读(Repeatable Read)
- 串行化(Serializable)
每个隔离级别处理以下三类问题的方式不同:
- 脏读(Dirty Read):一个事务读取了另一个事务未提交的数据。
- 不可重复读(Non-repeatable Read):一个事务在执行过程中读取了同一数据的不同值。
- 幻读(Phantom Read):一个事务在执行过程中读取到了另一个事务插入的新数据。
1. 未提交读(Read Uncommitted)
- 特点:最低的隔离级别,事务可以读取其他未提交事务的数据。
- 问题:可能会出现脏读、不可重复读和幻读。
- 应用场景:几乎不使用,因为它不能保证数据一致性。
示例
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
2. 提交读(Read Committed)
- 特点:一个事务只能读取已提交的另一个事务的数据。这是大多数数据库系统的默认隔离级别。
- 问题:可以避免脏读,但可能会出现不可重复读和幻读。
- 应用场景:适用于大多数应用,因为它在性能和数据一致性之间取得了平衡。
示例
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
3. 可重复读(Repeatable Read)
- 特点:在同一个事务中多次读取同一数据时,保证读取到的值是相同的。
- 问题:可以避免脏读和不可重复读,但可能会出现幻读。
- 应用场景:适用于需要强一致性的场景。MySQL InnoDB 存储引擎的默认隔离级别。
示例
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
4. 序列化(Serializable)
- 特点:最高的隔离级别,完全锁定读取的数据,防止其他事务并发操作,保证事务完全串行化执行。
- 问题:避免了脏读、不可重复读和幻读,但性能较差,可能导致大量锁争用。
- 应用场景:适用于需要绝对一致性且并发量较低的场景。
示例
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
事务隔离级别的实现
事务隔离级别的实现主要依赖于锁机制和多版本并发控制(MVCC)。以下是这两种机制的基本概念:
- 锁机制:通过对数据行或表加锁,防止其他事务同时访问相同的数据。
- 多版本并发控制(MVCC):通过保留数据的多个版本,使读取操作可以在不阻塞写入操作的情况下完成。
锁机制
- 共享锁(Shared Lock,S锁):允许事务读取数据,阻止其他事务对数据进行修改。
- 排他锁(Exclusive Lock,X锁)[ɪkˈskluːsɪv lɒk]:允许事务读写数据,阻止其他事务访问该数据。
MVCC
- 读取视图(Read View):每个事务读取时,生成一个一致性视图,确保读取到的数据是事务启动时的快照。
- 版本链:每个数据行保留多个版本,每个版本包含创建时间和删除时间(事务ID)。
总结
选择合适的事务隔离级别可以在性能和一致性之间取得平衡。一般而言:
- 未提交读:不建议使用。
- 提交读:大多数应用的默认选择,适合对一致性要求不高的场景。
- 可重复读:适合对一致性要求较高的场景,避免了脏读和不可重复读。
- 序列化:适合对一致性要求最高的场景,但性能较差,慎用。
了解事务隔离级别及其实现机制有助于根据应用需求优化数据库性能和一致性。
说一下MySQL常用的引擎
1. InnoDB
特点
- 事务支持:支持 ACID 事务(Atomicity, Consistency, Isolation, Durability)。
- 行级锁:提供高并发性,使用行级锁来提高性能。
- 外键约束:支持外键约束,确保数据的参照完整性。
- 自动崩溃恢复:提供自动崩溃恢复功能。
- MVCC:使用多版本并发控制(MVCC)来实现高并发。
适用场景
- 高并发写操作的应用场景。
- 需要事务支持的应用场景。
- 需要数据一致性的应用场景。
示例
CREATE TABLE mytable (
id INT PRIMARY KEY,
name VARCHAR(100)
) ENGINE=InnoDB;
2. MyISAM
特点
- 非事务性:不支持事务。
- 表级锁:使用表级锁,写操作性能较低。
- 全文索引:支持全文索引,适用于文本搜索。
- 存储小:相比 InnoDB,占用的存储空间较小。
适用场景
- 以读操作为主的应用场景。
- 需要快速读写性能但不需要事务支持的应用场景。
- 日志、数据仓库等场景。
说一下 mysql 的行锁和表锁?
行锁(Row Lock)
特点
- 粒度小:锁定特定的数据行,而不是整个表。
- 并发高:允许更多的并发操作,因为只锁定了相关的行。
- 事务支持:InnoDB 引擎支持行锁,适用于需要高并发和事务支持的应用。
使用场景
- 适用于需要高并发写操作的应用。
- 事务操作频繁的场景。
- 多用户并发访问同一表的不同数据行。
示例
-- 更新某一行数据,只锁定该行
START TRANSACTION;
UPDATE employees SET salary = salary + 1000 WHERE employee_id = 1;
COMMIT;
实现机制
- InnoDB:通过索引来实现行锁。如果表没有索引或查询条件未使用索引,InnoDB 会退化为表锁。
表锁(Table Lock)
特点
- 粒度大:锁定整个表。
- 并发低:较低的并发性,因为其他事务在等待当前事务释放表锁时无法访问该表。
- 简单高效:适用于读多写少的场景,开销较小。
使用场景
- 读多写少的应用。
- 数据导入、表结构变更等操作需要锁定整个表的场景。
- MyISAM 引擎使用表锁适用于简单的查询和分析操作。
示例
-- 锁定整个表进行操作
LOCK TABLES employees WRITE;
UPDATE employees SET salary = salary + 1000 WHERE employee_id = 1;
UNLOCK TABLES;
mysql 问题排查都有哪些手段?
- 使用 show processlist 命令查看当前所有连接信息。
- 使用 explain 命令查询 SQL 语句执行计划。
- 开启慢查询日志,查看慢查询的 SQL。
视图
视图(View)是 SQL 中的一种虚拟表,它是基
于一个或多个表(或视图)的查询结果集。视图可以简化复杂的查询、提供数据安全性和增强可维护性。下面是关于视图的一些关键概念、优点、限制和示例。
视图的基本概念
视图是一个虚拟表,不存储实际的数据,而是存储查询定义。查询视图时,数据库会根据视图定义动态生成数据。
创建视图
使用 CREATE VIEW
语句创建视图:
CREATE VIEW view_name AS
SELECT column1, column2, ...
FROM table_name
WHERE condition;
示例
假设有一个 employees
表:
CREATE TABLE employees (
id INT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
department VARCHAR(50),
salary DECIMAL(10, 2)
);
创建一个视图 high_salary_employees
,显示工资高于 5000 的员工信息:
CREATE VIEW high_salary_employees AS
SELECT first_name, last_name, department, salary
FROM employees
WHERE salary > 5000;
查询视图:
SELECT * FROM high_salary_employees;
视图的优点
- 简化复杂查询:视图可以封装复杂的查询逻辑,简化应用程序中的 SQL 语句。
- 数据安全性:视图可以限制用户访问表中的特定数据,提高数据安全性。
- 数据抽象:视图提供数据抽象层,应用程序无需了解底层表结构的变化。
- 数据一致性:通过视图,可以确保多个查询使用相同的逻辑,从而保持数据一致性。
视图的限制
- 性能:由于视图是基于查询动态生成的,复杂视图可能会影响查询性能。
- 不可更新视图:并非所有视图都是可更新的,某些视图(如包含聚合函数的视图)不能直接更新。
- 依赖性:视图依赖于基础表,基础表的更改可能影响视图的有效性。
可更新视图
视图通常是只读的,但在某些情况下,可以通过视图更新基础表的数据。可更新视图的条件:
- 视图中包含基础表的主键。
- 视图不包含聚合函数、DISTINCT、GROUP BY、HAVING、UNION 或子查询。
- 对于 JOIN 视图,更新必须能够明确地映射到一个基础表。
示例
创建一个可更新视图:
CREATE VIEW employee_salaries AS
SELECT id, first_name, last_name, salary
FROM employees;
更新视图中的数据:
UPDATE employee_salaries
SET salary = 6000
WHERE id = 1;
删除视图
使用 DROP VIEW
语句删除视图:
DROP VIEW view_name;
视图的实际应用
- 简化查询:使用视图封装复杂的查询逻辑。
- 数据安全:限制用户只能通过视图访问特定的数据列。
- 报表生成:视图可用于生成复杂的报表数据。
例子:简化复杂查询
假设有两个表 employees
和 departments
:
CREATE TABLE departments (
id INT PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE employees (
id INT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
department_id INT,
salary DECIMAL(10, 2),
FOREIGN KEY (department_id) REFERENCES departments(id)
);
创建一个视图,显示员工的完整信息,包括部门名称:
CREATE VIEW employee_details AS
SELECT e.id, e.first_name, e.last_name, d.name AS department_name, e.salary
FROM employees e
JOIN departments d ON e.department_id = d.id;
查询视图:
SELECT * FROM employee_details;
通过视图,可以简化应用程序中的 SQL 语句,并确保数据的一致性和安全性。
触发器(Trigger)是数据库系统中的一种特殊存储过程,在指定的表上执行特定的事件(如插入、更新或删除)时自动触发和执行。触发器常用于确保数据完整性、自动化复杂业务逻辑以及执行审计任务。
触发器的基本概念
触发器与特定的表关联,在该表上执行特定类型的操作时(例如 INSERT、UPDATE、DELETE),触发器会自动执行预定义的 SQL 代码。
触发器的类型
根据触发时机和事件类型,触发器可以分为以下几种:
- BEFORE 触发器:在执行指定的事件之前触发。
- AFTER 触发器:在执行指定的事件之后触发。
触发器的语法
创建触发器的基本语法如下:
CREATE TRIGGER trigger_name
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON table_name
FOR EACH ROW
BEGIN
-- 触发器逻辑
END;
示例
假设有一个 employees
表,我们希望在插入新员工记录时,自动在 audit_log
表中记录此操作。
创建示例表
CREATE TABLE employees (
id INT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
department VARCHAR(50),
salary DECIMAL(10, 2)
);
CREATE TABLE audit_log (
log_id INT PRIMARY KEY AUTO_INCREMENT,
operation VARCHAR(50),
employee_id INT,
timestamp DATETIME
);
创建触发器
创建一个在插入新员工记录之前触发的触发器:
CREATE TRIGGER before_employee_insert
BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
INSERT INTO audit_log (operation, employee_id, timestamp)
VALUES ('INSERT', NEW.id, NOW());
END;
触发器的作用
- 数据完整性:确保业务规则和数据完整性。例如,防止非法数据插入或更新。
- 自动化任务:自动执行审计、日志记录或计算派生数据。
- 复杂业务逻辑:在数据库层面实现复杂的业务逻辑,减少应用程序代码的复杂性。
触发器的注意事项
- 性能:触发器可能会影响数据库的性能,特别是在高并发的环境中,因为每次触发事件都会执行预定义的 SQL 代码。
- 调试和维护:触发器的调试和维护可能较为复杂,因为触发器逻辑是自动执行的,容易被忽略。
- 递归触发器:需要小心处理触发器中的递归调用,以防止无限递归和堆栈溢出。
示例:UPDATE 触发器
假设我们希望在更新员工工资时记录此操作:
CREATE TRIGGER after_employee_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
INSERT INTO audit_log (operation, employee_id, timestamp)
VALUES ('UPDATE', NEW.id, NOW());
END;
查看和删除触发器
查看表上的触发器:
SHOW TRIGGERS LIKE 'employees';
删除触发器:
DROP TRIGGER before_employee_insert;
触发器示例总结
触发器可以大大简化数据完整性、审计和复杂业务逻辑的实现,但在使用时需要注意性能和维护问题。以下是一个完整的示例:
-- 创建 employees 表
CREATE TABLE employees (
id INT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
department VARCHAR(50),
salary DECIMAL(10, 2)
);
-- 创建 audit_log 表
CREATE TABLE audit_log (
log_id INT PRIMARY KEY AUTO_INCREMENT,
operation VARCHAR(50),
employee_id INT,
timestamp DATETIME
);
-- 创建 BEFORE INSERT 触发器
CREATE TRIGGER before_employee_insert
BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
INSERT INTO audit_log (operation, employee_id, timestamp)
VALUES ('INSERT', NEW.id, NOW());
END;
-- 创建 AFTER UPDATE 触发器
CREATE TRIGGER after_employee_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
INSERT INTO audit_log (operation, employee_id, timestamp)
VALUES ('UPDATE', NEW.id, NOW());
END;
通过这种方式,可以确保在插入和更新员工数据时,自动记录审计日志
MySQL中隐式转换
在 MySQL 中,隐式转换(Implicit Conversion)指的是在表达式中不同数据类型之间的自动转换。这种转换由 MySQL 自动完成,目的是为了使表达式的计算能够顺利进行。MySQL 会在以下情况下进行隐式转换:
- 比较操作:当两个不同类型的数据进行比较时。
- 赋值操作:当不同类型的数据进行赋值时。
- 函数调用:当函数参数的数据类型与期望的数据类型不一致时。
常见的隐式转换规则
数值类型之间的转换
- MySQL 会在数值类型之间进行自动转换,例如在
INT
和FLOAT
之间。 - 小范围的数据类型可以自动转换为大范围的数据类型,例如
TINYINT
转换为INT
。
字符串和数值类型之间的转换
- 如果字符串包含合法的数值表示,MySQL 会将字符串转换为相应的数值类型。
- 如果字符串不包含合法的数值表示,MySQL 会将其转换为
0
。
日期和字符串之间的转换
- MySQL 会将日期字符串自动转换为日期类型,例如
'2024-07-03'
自动转换为DATE
类型。 - 反之,日期类型可以自动转换为字符串。
示例
数值类型之间的转换
CREATE TABLE test_numeric_conversion (
a INT,
b FLOAT
);
INSERT INTO test_numeric_conversion (a, b) VALUES (1, 2.5);
SELECT a + b FROM test_numeric_conversion; -- 结果是 3.5,INT 被隐式转换为 FLOAT
在上面的例子中,INT
类型的列 a
与 FLOAT
类型的列 b
相加时,a
被隐式转换为 FLOAT
类型。
字符串和数值类型之间的转换
CREATE TABLE test_string_conversion (
a VARCHAR(10),
b INT
);
INSERT INTO test_string_conversion (a, b) VALUES ('123', 456);
INSERT INTO test_string_conversion (a, b) VALUES ('abc', 789);
SELECT a + b FROM test_string_conversion; -- 结果分别是 579 和 789,字符串 'abc' 被转换为 0
在上面的例子中,字符串 '123'
被隐式转换为整数 123
,而字符串 'abc'
被转换为整数 0
。
日期和字符串之间的转换
CREATE TABLE test_date_conversion (
a DATE,
b VARCHAR(10)
);
INSERT INTO test_date_conversion (a, b) VALUES ('2024-07-03', '2024-07-04');
SELECT a + INTERVAL 1 DAY FROM test_date_conversion; -- 结果是 '2024-07-04'
SELECT b + INTERVAL 1 DAY FROM test_date_conversion; -- 结果是 '2024-07-05'
在上面的例子中,日期字符串 '2024-07-03'
和 '2024-07-04'
分别被隐式转换为 DATE
类型。
注意事项
隐式转换虽然方便,但也可能带来一些问题:
- 性能问题:隐式转换可能导致索引失效,从而影响查询性能。
- 数据精度问题:隐式转换可能导致数据精度丢失。
- 不可预期的结果:隐式转换可能导致一些不可预期的结果,特别是在字符串和数值类型之间的转换。
-
避免隐式转换的方法
为了避免隐式转换带来的问题,建议在 SQL 语句中显式地进行类型转换。MySQL 提供了多种类型转换函数,例如
CAST
和CONVERT
。显式转换示例
SELECT CAST(a AS UNSIGNED) + b FROM test_string_conversion;
SELECT CONVERT(a, DECIMAL(10, 2)) + b FROM test_numeric_conversion;
通过显式地进行类型转换,可以提高 SQL 语句的可读性和可维护性,同时避免隐式转换带来的问题
数值类型的隐式转换顺序
MySQL 的数值类型从小范围到大范围的转换顺序如下:
TINYINT tinyint
SMALLINT smallint
MEDIUMINT mediumint
INT
(或INTEGER
) intBIGINT bigint
FLOAT float
DOUBLE double
DECIMAL declmal
锁
在数据库系统中,锁是一种用于管理并发访问和维护数据完整性的重要机制。锁可以防止多个事务同时访问同一资源(如表或行)时导致的数据不一致或竞争条件。锁的实现和管理对于数据库的性能和可靠性至关重要。
锁的类型
根据锁的粒度和作用,可以将锁分为多种类型:
- 行锁(Row Lock):锁住特定的行。
- 表锁(Table Lock):锁住整个表。
- 页锁(Page Lock):锁住数据库页(通常是多个行)。
- 意向锁(Intention Lock):表示事务打算对某些行或页进行更细粒度的锁定。
根据锁的模式,可以分为:
- 共享锁(Shared Lock, S 锁):允许多个事务同时读取,但不允许写。
- 排他锁(Exclusive Lock, X 锁):独占锁,允许事务读和写,但其他事务不能读或写。
- 意向共享锁(Intention Shared Lock, IS 锁):表示事务意图对某些行加共享锁。
- 意向排他锁(Intention Exclusive Lock, IX 锁):表示事务意图对某些行加排他锁。
行锁和表锁
行锁
行锁是锁粒度最细的一种锁,主要用于高并发环境下,能够最大限度地并发访问不同的数据行。
优点:
- 高并发性:多个事务可以同时锁定不同的行。
- 精确控制:只锁定需要的行,减少对其他事务的影响。
缺点:
- 开销较大:需要跟踪更多的锁信息。
- 死锁风险:多个事务可能会导致死锁。
表锁
表锁是锁粒度较粗的一种锁,主要用于低并发环境或大批量操作时。
优点:
- 管理简单:只需要管理少量锁信息。
- 死锁风险低:因为锁粒度大,事务间竞争较少。
缺点:
- 并发性低:所有操作都要等待表锁释放。
- 粒度较粗:可能会导致更多的事务等待。
MySQL中的锁
在MySQL中,不同存储引擎对锁的支持有所不同:
InnoDB 引擎
InnoDB 是 MySQL 默认的事务型存储引擎,支持行锁和表锁。
- 行锁:InnoDB 主要使用行锁来提高并发性。
- 表锁:在某些操作中,如
ALTER TABLE
,InnoDB 也会使用表锁。
MyISAM 引擎
MyISAM 是 MySQL 的非事务型存储引擎,只支持表锁。
- 表锁:MyISAM 主要使用表锁,对于读操作使用共享锁,对于写操作使用排他锁。
锁的实现和管理
锁的设置和查看
InnoDB存储引擎自动管理锁,但可以通过一些SQL命令和系统表查看和管理锁。
查看当前锁信息:
SHOW ENGINE INNODB STATUS;
ENGINE engine [ˈendʒɪn]
查看锁等待和死锁信息:
SELECT * FROM information_schema.innodb_locks;
SELECT * FROM information_schema.innodb_lock_waits;
手动加锁和解锁
在事务中,可以手动加锁来控制并发行为:
-- 显式加锁
LOCK TABLES table_name READ; -- 读锁
LOCK TABLES table_name WRITE; -- 写锁
-- 解锁
UNLOCK TABLES;
在InnoDB中,也可以使用FOR UPDATE和LOCK IN SHARE MODE来加锁:
-- 加排他锁
SELECT * FROM employees WHERE id = 1 FOR UPDATE;
-- 加共享锁
SELECT * FROM employees WHERE id = 1 LOCK IN SHARE MODE;
死锁和死锁避免
死锁
死锁是指两个或多个事务在等待对方持有的锁,导致彼此无法继续执行的情况。
避免死锁的方法
- 获取锁的顺序一致:确保所有事务按相同的顺序获取锁,减少死锁的可能性。
- 短事务:尽量减少事务执行时间,减少锁的持有时间。
- 适当的锁粒度:根据实际情况选择合适的锁粒度。
- 死锁检测:使用数据库的死锁检测机制,及时发现并解决死锁。
示例
行锁示例
假设有一个 employees
表:
CREATE TABLE employees (
id INT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
salary DECIMAL(10, 2)
) ENGINE=InnoDB;
在事务中使用行锁:
START TRANSACTION;
SELECT * FROM employees WHERE id = 1 FOR UPDATE;
-- 对记录进行更新操作
UPDATE employees SET salary = salary + 1000 WHERE id = 1;
COMMIT;
表锁示例
对表进行加锁和解锁:
LOCK TABLES employees WRITE;
-- 对表进行更新操作
UPDATE employees SET salary = salary + 1000 WHERE id = 1;
UNLOCK TABLES;
总结
锁是数据库管理系统中至关重要的机制,用于管理并发访问和维护数据完整性。不同的锁类型(如行锁、表锁)和锁模式(如共享锁、排他锁)在不同的应用场景中发挥作用。了解和正确使用锁机制,可以有效提高数据库系统的性能和可靠性。
提交(Commit)
提交是指将事务中所有的操作永久保存到数据库中,并结束事务。提交操作会将事务所做的修改从临时状态转为永久状态,其他事务可以看到这些修改。
在大多数数据库中,提交事务使用 commit
命令或通过编程接口调用 commit()
方法。例如,在 JDBC 中:
connection.commit(); // 提交事务
回滚(Rollback)
回滚是指撤销事务中已经执行的操作,将数据库恢复到事务开始前的状态。回滚操作通常发生在事务执行过程中出现错误或者由于业务逻辑需要撤销当前的操作时。
在数据库中,回滚事务使用 rollback
命令或通过编程接口调用 rollback()
方法。例如,在 JDBC 中:
connection.rollback(); // 回滚事务
示例
以下是一个简单的示例,展示了如何使用 JDBC 中的事务提交和回滚:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class TransactionExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/test";
String user = "username";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, user, password)) {
conn.setAutoCommit(false); // 设置为手动提交模式
try (Statement stmt = conn.createStatement()) {
// 执行一些 SQL 操作
stmt.executeUpdate("INSERT INTO employees (id, name, salary) VALUES (1, 'Alice', 5000)");
stmt.executeUpdate("INSERT INTO employees (id, name, salary) VALUES (2, 'Bob', 6000)");
conn.commit(); // 提交事务
System.out.println("事务已提交");
} catch (SQLException ex) {
conn.rollback(); // 回滚事务
System.out.println("事务已回滚");
ex.printStackTrace();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
在上述示例中,通过设置 conn.setAutoCommit(false)
将自动提交模式关闭,显式调用 conn.commit()
提交事务,以及在发生异常时调用 conn.rollback()
进行事务回滚。这样可以确保操作的原子性和一致性,避免数据不一致性的情况发生。
Java Web
1.jsp 和 servlet /'sɝvlɛt/有什么区别?
JSP (JavaServer Pages) 和 Servlet 是 Java EE 中用于创建动态 web 内容的技术。虽然它们有很多相似之处,但它们也有显著的区别。以下是 JSP 和 Servlet 的主要区别:
概述
- Servlet:是一种 Java 编程技术,用于在 web 服务器上生成动态内容。Servlet 是在服务器端运行的 Java 类,可以处理客户端请求并生成响应。
- JSP:是一种基于 Servlet 的技术,它允许在 HTML 页面中嵌入 Java 代码。JSP 页面最终会被转换成 Servlet,并由服务器执行。
主要区别
-
编写方式:
- Servlet:是纯 Java 代码。开发者需要用 Java 编写业务逻辑和 HTML 内容。
- JSP:主要是 HTML 代码中嵌入少量的 Java 代码(使用 JSP 标签和表达式)。它更像是 HTML 页面。
-
代码维护:
- Servlet:由于大量的 HTML 和 Java 代码混杂在一起,代码的可读性和维护性较差。
- JSP:通过将业务逻辑和表示层分离,维护起来更容易。可以使用 JSTL 和自定义标签库来减少 Java 代码的使用。
-
转译和编译:
- Servlet:直接编译成字节码,由服务器执行。
- JSP:在第一次请求时,由服务器将 JSP 页面转译成 Servlet,然后再编译执行。
-
使用场景:
- Servlet:适用于业务逻辑较多的场景,例如处理表单数据、处理请求和响应等。
- JSP:适用于显示数据的场景,例如创建动态网页内容、生成 HTML 页面等。
-
生命周期:
- Servlet:由容器管理其生命周期,包括加载、实例化、初始化(
init
方法)、请求处理(service
方法)、销毁(dest
roy
方法)。 - JSP:也由容器管理,但在第一次请求时会转译成 Servlet。JSP 页面对应的 Servlet 类具有与普通 Servlet 相同的生命周期方法。
- Servlet:由容器管理其生命周期,包括加载、实例化、初始化(
-
表达能力:
- Servlet:更适合处理复杂的逻辑、控制流和处理 HTTP 请求和响应。
- JSP:更适合编写 HTML,并且可以使用 JSTL(JSP Standard Tag Library)和自定义标签来减少 Java 代码的嵌入。
示例
Servlet 示例:
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
response.getWriter().println("<html><body>");
response.getWriter().println("<h1>Hello, World!</h1>");
response.getWriter().println("</body></html>");
}
}
JSP 示例:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Hello JSP</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
总结
- Servlet 更适合处理复杂的业务逻辑和控制流,而 JSP 更适合页面展示和用户界面设计。
- 在实际应用中,通常会结合使用 Servlet 和 JSP,将业务逻辑放在 Servlet 中处理,然后将结果转发到 JSP 页面进行展示,从而实现 MVC(Model-View-Controller)模式。
Ajax 相关面试问题
1. 什么是 Ajax?
Ajax(Asynchronous JavaScript and XML)是一种无需重新加载整个网页就可以更新部分网页内容的技术。它通过使用 XMLHttpRequest 对象与服务器进行异步通信,实现页面的局部刷新。
2. Ajax 的工作原理
- 创建 XMLHttpRequest 对象:在浏览器中创建一个 XMLHttpRequest 对象。
- 设置请求参数:使用 open() 方法设置请求的类型、URL 以及是否异步。
- 发送请求:使用 send() 方法向服务器发送请求。
- 处理响应:通过监听 readyState 变化,处理服务器响应的数据。
3. Ajax 的优点
- 提高用户体验:页面部分更新而不需要整体刷新,提升交互体验。
- 减少带宽使用:只传输需要的数据,减少带宽消耗。
- 异步处理:请求异步进行,不会阻塞用户操作。
4. Ajax 的缺点
- SEO:由于部分内容通过 JavaScript 加载,搜索引擎可能无法抓取这些内容。
- 安全性:Ajax 请求可能会带来安全风险,需要注意防范跨站请求伪造(CSRF)等攻击。
- 浏览器兼容性:旧版浏览器可能不完全支持 Ajax。
1. 什么是 Ajax?它的主要作用是什么?
回答要点:
- Ajax 是一种无需重新加载整个网页就可以更新部分网页内容的技术。
- 主要作用是提升用户体验,实现异步数据交互。
2. 解释 Ajax 的工作原理。
回答要点:
- 创建 XMLHttpRequest 对象。
- 使用 open() 方法设置请求参数。
- 使用 send() 方法发送请求。
- 监听 readyState 变化并处理响应数据。
3. 如何创建一个简单的 Ajax 请求?
回答要点:
var xhr = new XMLHttpRequest();
xhr.open("GET", "example.txt", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
};
xhr.send();
4. jQuery 中的 $.ajax() 方法有哪些常用参数?
回答要点:
- url
- type (或 method)
- data
- dataType
- async
- timeout
- headers
- beforeSend
- success
- error
- complete
5. 如何处理 Ajax 请求中的错误?M
回答要点:
8. 如何使用 jQuery 实现跨域 Ajax 请求?
回答要点:
- 使用 XMLHttpRequest 对象的 onerror 事件。
- 在 jQuery 的 $.ajax() 方法中使用 error 回调函数。
$.ajax({ url: "example.txt", type: "GET", success: function(data) { console.log(data); }, error: function(jqXHR, textStatus, errorThrown) { console.error("Error: " + textStatus + ", " + errorThrown); } });
6. 如何避免 Ajax 请求中的缓存问题?
回答要点:
- 使用唯一的查询参数,例如时间戳
var xhr = new XMLHttpRequest(); xhr.open("GET", "example.txt?t=" + new Date().getTime(), true); xhr.send();
在 jQuery 中设置 cache 参数为 false。
$.ajax({ url: "example.txt", type: "GET", cache: false, success: function(data) { console.log(data); } });
7. 解释 JSONP 的原理及其使用场景。
回答要点:
- JSONP(JSON with Padding)是一种解决跨域请求的技术。
- 通过动态创建
<script>
标签来请求数据,服务器返回 JSON 数据并调用指定的回调函数。 - 使用场景:需要跨域请求数据,且服务器支持 JSONP。
- 使用 JSONP。
$.ajax({ url: "https://example.com/data", type: "GET", dataType: "jsonp", success: function(data) { console.log(data); } });
9. 解释同步和异步请求的区别。
回答要点:
- 同步请求:在请求完成之前,浏览器会锁住页面,用户无法进行其他操作。
- 异步请求:请求在后台进行,不会阻塞用户操作。
10. 什么是跨域请求?如何解决 Ajax 跨域请求的问题?
回答要点:
- 跨域请求:浏览器安全机制限制,网页只能向同域名的服务器发送请求。
- 解决方法:
- JSONP(仅支持 GET 请求)
- CORS(跨域资源共享,服务器需配置允许跨域)
- 代理服务器
jsp 有哪些内置对象?作用分别是什么?
request:用户端请求(get/post)
response:网页传回用户端的响应
pageContext:管理网页的属性
session:与请求有关的会话期
application servlet:正在执行的内容
out:用来传送回应的输出
config: servlet的架构部件
page JSP:网页本身
exception:针对错误网页,未捕捉的例外
3.说一下 jsp 的 4 种作用域?
application:在所有应用程序中有效
session:在当前会话中有效;
request:在当前请求中有效;
page:在当前页面有效
4.session 和 cookie 有什么区别?
存储位置不同
cookie的数据信息存放在客户端浏览器上
session的数据信息存放在服务器上
存储容量不同
单个cookie保存的数据<=4Kb,一个站点最多保存20个cookie
对于session来说没有上限
存储方式不同
cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据;
session中能够存储任何类型的数据
隐私策略不同
cookie对客户端是可见的,所以它是不安全的;
session存储在服务器上,对客户端是透明的,不会信息泄露
有效期不同
可以通过设置cookie的属性,使cookie长期有效;
session不能达到长期有效的结果
服务器压力不同
cookie保存在客户端,不占用服务器资源
session是保存在服务器的,如果并发访问的用户十分多,会产生很多的session,耗费大量的内存
浏览器支持不同
cookie是需要浏览器支持的,假如客户端禁用了cookie,或者不支持cookie,则会话跟踪会失效
session只能在本窗口以及子窗口有效,而cookie可以为本浏览器上的一切窗口有效
跨域支持不同
cookie支持跨域名访问
session不支持跨域名访问
5.说一下 session 的工作原理?
客户端登录完成之后,服务器会创建相应的session,session创建完成之后,会把session的id发送给客户端,客户端再存储到浏览器中。这样客户端每次访问服务器时,都会带着sessionid,服务器拿到sessionid之后,在内存找到与之对应的session就可以正常工作了。
6.如果客户端禁止 cookie,session 还能用吗?
如果浏览器禁用了cookie,那么客户端访问服务端时无法携带sessionid,服务端无法识别用户身份,便无法进行会话控制,session就会失效,但是可以通过其他办法实现:
通过URL重写,把sessionid作为参数追加的原URL中,后续的浏览器与服务器交互中携带session
服务器的返回数据中包含sessionid,浏览器发送请求时,携带sessionid参数
7.spring mvc 和 struts 的区别是什么?
拦截机制不同
Struts2是类级别的拦截,每次请求就会创建一个Action
SpringMVC是方法级别的拦截,一个方法对应一个Request上下文。
底层框架不同
Struts2采用Filter实现,SpringMVC采用Servlet实现
性能不同
Struts2需要加载所有的属性值注入,SpringMVC实现了零配置,由于SpringMVC基于方法的拦截。
所以,SpringMVC开发效率和性能高于Struts2
配置方面
SpringMVC 和 Spring 是无缝的,从这个项目的管理和安全上也比Struts2高。
8.如何避免 sql 注入?
SQL注入是Web开发中最常见的一种安全漏洞,可以用它来从数据库获取敏感信息,进行数据库的一系列非法操作。
校验参数的数据格式是否合法
对进入数据库的特殊字符进行转义处理,或编码转换
预编译SQL,参数化查询方式,避免SQL拼接
发布前利用工具进行SQL注入检测
9.什么是 XSS 攻击,如何避免?
XSS(Cross Site Scripting)跨站脚本攻击,它是Web程序中常见的漏洞。
原理:攻击者往web页面里插入恶意的HTML代码,当用户浏览该页面时,嵌入其这个你的HTML代码会被执行,从而达到恶意攻击用户的目的。
避免措施:
web页面中可由用户输入的地方,对输入的数据转义、过滤处理
前端对HTML标签属性、css属性赋值的地方进行校验
后台输出页面的时候,也需要对输出内容进行转义、过滤处理
10.什么是 CSRF 攻击,如何避免?
CSRF(Cross-site request forgery),也被称为one-click attack 或者 session riding,通常缩写为CSRF或XSRF,
是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。
预防措施:
验证HTTP Referer字段
验证码
添加token验证
尽量使用post,限制get
网络
TCP三次握手
TCP三次握手(Three-Way Handshake)是TCP协议建立连接时所使用的过程。它确保客户端和服务器都能够发送和接收数据,并且双方的初始序列号(ISN)已被正确同步。以下是三次握手的详细步骤:
1. 第一次握手:SYN
客户端向服务器发送一个SYN(同步序列号)报文段。这个报文段指示客户端希望发起连接,并且包含一个初始序列号(ISN)。
Client: SYN, Seq = X
2. 第二次握手:SYN-ACK
服务器收到SYN报文段后,回应一个SYN-ACK(同步-确认)报文段。这个报文段包含服务器的初始序列号(ISN)以及对客户端SYN报文段的确
认序列号(ACK)
Server: SYN, Seq = Y, ACK = X+1
3. 第三次握手:ACK
客户端收到SYN-ACK报文段后,再次发送一个ACK(确认)报文段给服务器。这个报文段包含客户端的序列号和对服务器SYN报文段的确认序列号。
Client: ACK, Seq = X+1, ACK = Y+1
在这三个步骤完成后,客户端和服务器之间的TCP连接就建立起来了,双方都可以开始发送数据。
三次握手过程的详细解释
-
客户端发送SYN报文段:
- 客户端向服务器发送一个SYN报文段,标志着客户端希望发起连接。
- 报文段包含客户端的初始序列号(ISN),例如X。
-
服务器回复SYN-ACK报文段:
- 服务器收到客户端的SYN报文段后,发送一个SYN-ACK报文段。
- 报文段包含服务器的初始序列号(例如Y),以及对客户端初始序列号的确认(X+1)。
-
客户端回复ACK报文段:
- 客户端收到服务器的SYN-ACK报文段后,发送一个ACK报文段给服务器。
- 报文段包含对服务器初始序列号的确认(Y+1)。
这三个步骤完成后,TCP连接建立,客户端和服务器之间可以开始数据传输。
为什么需要三次握手?
- 可靠性:三次握手确保双方能够收发数据,并确认彼此的初始序列号,从而同步双方的发送和接收状态。
- 防止旧连接干扰:三次握手可以避免由于延迟的旧连接请求报文干扰新的连接请求。旧的连接请求可能仍在网络中传输,三次握手确保每次连接请求都是新的。
三次握手示意图
Client Server
| |
| ----------- SYN, Seq = X ---------->|
| |
| <----- SYN, Seq = Y, ACK = X+1 -----|
| |
| ----------- ACK, Seq = X+1 -------->|
| |
代码示例
以下是使用Java实现TCP三次握手的简单示例代码:
服务器端:
import java.io.*;
import java.net.*;
public class TCPServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("Server is listening on port 8080");
Socket socket = serverSocket.accept();
System.out.println("New client connected");
InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true);
String text;
do {
text = reader.readLine();
System.out.println("Received from client: " + text);
writer.println("Server: " + text);
} while (!text.equals("bye"));
socket.close();
} catch (IOException ex) {
System.out.println("Server exception: " + ex.getMessage());
ex.printStackTrace();
}
}
}
客户端:
import java.io.*;
import java.net.*;
public class TCPClient {
public static void main(String[] args) {
String hostname = "localhost";
int port = 8080;
try (Socket socket = new Socket(hostname, port)) {
OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true);
InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
String text;
do {
System.out.print("Enter message: ");
text = consoleReader.readLine();
writer.println(text);
String response = reader.readLine();
System.out.println(response);
} while (!text.equals("bye"));
socket.close();
} catch (UnknownHostException ex) {
System.out.println("Server not found: " + ex.getMessage());
} catch (IOException ex) {
System.out.println("I/O error: " + ex.getMessage());
}
}
}
通过这个简单的客户端和服务器端代码示例,展示了TCP连接的建立和数据传输的基本过程,底层的三次握手是由Java的Socket API自动处理的。
TCP 四次挥手
TCP 四次挥手(Four-Way Handshake)是用于断开 TCP 连接的过程,确保双方能够正常关闭连接并释放资源。四次挥手的步骤如下:
1. 第一次挥手:FIN
客户端发送一个 FIN(Finish)报文段,表示客户端不再发送数据了,但仍然可以接收数据。
Client: FIN, Seq = X
2. 第二次挥手:ACK
服务器收到 FIN 报文段后,发送一个 ACK(确认)报文段,确认客户端的 FIN 报文段。
Server: ACK, Seq = Y, ACK = X+1
3. 第三次挥手:FIN
服务器发送一个 FIN 报文段,表示服务器也不再发送数据了。
Server: FIN, Seq = Z
4. 第四次挥手:ACK
客户端收到服务器的 FIN 报文段后,发送一个 ACK 报文段,确认服务器的 FIN 报文段。
Client: ACK, Seq = X+1, ACK = Z+1
四次挥手过程的详细解释
-
客户端发送 FIN 报文段:
- 客户端向服务器发送一个 FIN 报文段,表示客户端完成数据发送。
- 报文段包含客户端的序列号(例如 X)。
-
服务器回复 ACK 报文段:
- 服务器收到客户端的 FIN 报文段后,发送一个 ACK 报文段。
- 报文段包含对客户端 FIN 报文段的确认序列号(X+1)。
-
服务器发送 FIN 报文段:
- 服务器发送一个 FIN 报文段,表示服务器完成数据发送。
- 报文段包含服务器的序列号(例如 Z)。
-
客户端回复 ACK 报文段:
- 客户端收到服务器的 FIN 报文段后,发送一个 ACK 报文段。
- 报文段包含对服务器 FIN 报文段的确认序列号(Z+1)。
为什么需要四次挥手?
4o
- 确保双向关闭:四次挥手确保连接的双方都完成了数据传输并确认彼此的关闭请求。
- 防止数据丢失:确保所有数据都已经传输完毕,防止在连接关闭前丢失数据。
- 四次挥手示意图
Client Server | | | ----------- FIN, Seq = X ---------->| | | | <-------------- ACK --------------- | | Seq = Y, ACK = X+1 | | | | <----------- FIN, Seq = Z ----------| | | | ----------- ACK, Seq = X+1 -------->| | ACK = Z+1 |
代码示例
以下是使用 Java 实现 TCP 四次挥手的简单示例代码:
服务器端:
import java.io.*; import java.net.*; public class TCPServer { public static void main(String[] args) { try (ServerSocket serverSocket = new ServerSocket(8080)) { System.out.println("Server is listening on port 8080"); Socket socket = serverSocket.accept(); System.out.println("New client connected"); InputStream input = socket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(input)); OutputStream output = socket.getOutputStream(); PrintWriter writer = new PrintWriter(output, true); String text; do { text = reader.readLine(); System.out.println("Received from client: " + text); writer.println("Server: " + text); } while (!text.equals("bye")); // Close streams and socket reader.close(); writer.close(); socket.close(); System.out.println("Connection closed"); } catch (IOException ex) { System.out.println("Server exception: " + ex.getMessage()); ex.printStackTrace(); } } }
客户端:
import java.io.*; import java.net.*; public class TCPClient { public static void main(String[] args) { String hostname = "localhost"; int port = 8080; try (Socket socket = new Socket(hostname, port)) { OutputStream output = socket.getOutputStream(); PrintWriter writer = new PrintWriter(output, true); InputStream input = socket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(input)); BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in)); String text; do { System.out.print("Enter message: "); text = consoleReader.readLine(); writer.println(text); String response = reader.readLine(); System.out.println(response); } while (!text.equals("bye")); // Close streams and socket reader.close(); writer.close(); socket.close(); System.out.println("Connection closed"); } catch (UnknownHostException ex) { System.out.println("Server not found: " + ex.getMessage()); } catch (IOException ex) { System.out.println("I/O error: " + ex.getMessage()); } } }
通过这个简单的客户端和服务器端代码示例,展示了TCP连接的建立和数据传输的基本过程。底层的四次挥手是由Java的Socket API自动处理的。
四次挥手过程的现实意义
- 资源释放:四次挥手的过程确保连接双方正确释放连接资源,防止资源泄露。
- 防止数据丢失:通过确认每个阶段的关闭请求,保证所有数据包都已被接收和处理。
spring 常用的注入方式有哪些?
在 Spring 框架中,依赖注入(Dependency Injection, DI)是实现控制反转(Inversion of Control, IoC)的核心机制。Spring 提供了几种常见的依赖注入方式:
1. 构造函数注入
使用构造函数将依赖对象注入到类中。这种方式确保所有依赖在对象创建时就已经满足。
示例:
@Component
public class MyService {
private final MyRepository myRepository;
@Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
2. Setter 方法注入
通过 setter 方法注入依赖。这种方式允许在对象创建后注入依赖。
示例:
@Component
public class MyService {
private MyRepository myRepository;
@Autowired
public void setMyRepository(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
3. 字段注入
直接在字段上使用 @Autowired
注解注入依赖。这种方式简单直接,但不利于单元测试。
示例:
@Component
public class MyService {
@Autowired
private MyRepository myRepository;
}
4. 使用 @Resource
注解
@Resource
注解是 JSR-250 标准的一部分,可以用于注入依赖。它可以根据名称或类型进行注入。
示例:
@Component
public class MyService {
@Resource
private MyRepository myRepository;
}
5. 使用 @Value
注解
@Value
注解用于注入外部配置文件中的属性值,可以注入简单类型的值,如字符串、数字等。
示例:
@Component
public class MyService {
@Value("${my.property}")
private String myProperty;
}
6. 使用配置类注入
通过 Java 配置类使用 @Bean
注解定义和注入依赖。
示例:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyService(myRepository());
}
@Bean
public MyRepository myRepository() {
return new MyRepositoryImpl();
}
}
spring 中的 bean 是线程安全的吗?
在 Spring 框架中,默认情况下,Bean 是单例的(scope 为 singleton
),这意味着整个 Spring 容器中只有一个实例。对于这种单例 Bean,默认情况下并不是线程安全的,因为多个线程可以同时访问同一个实例。
Bean 的作用域
Spring 提供了几种不同的作用域来管理 Bean 的生命周期:
- Singleton(单例):默认作用域,Spring 容器中每个 Bean 只有一个共享的实例。
- Prototype(原型):每次请求都会创建一个新的 Bean 实例。
- Request:每次 HTTP 请求都会创建一个新的 Bean 实例(仅在 Web 应用中有效)。
- Session:每个 HTTP 会话都会创建一个新的 Bean 实例(仅在 Web 应用中有效)。
- GlobalSession:每个全局 HTTP 会话创建一个新的 Bean 实例(仅在 Web 应用中有效)。
单例 Bean 的线程安全性
单例 Bean 的线程安全性需要由开发者自己负责。以下是几种常见的处理线程安全问题的方法:
-
无状态的 Bean:如果 Bean 是无状态的(即它没有保存任何数据),那么它是线程安全的。例如,服务类只包含业务逻辑,不保存任何状态信息。
-
局部变量:将所有可变状态存储在方法的局部变量中,这样每个线程都有自己的变量副本,避免了并发访问问题。
-
同步代码块:使用同步代码块来控制对共享资源的访问。例如,可以使用
synchronized
关键字或java.util.concurrent.locks.Lock
来保护对共享状态的访问。 -
ThreadLocal:使用
ThreadLocal
类为每个线程提供独立的实例,从而避免并发访问问题。
spring 支持几种 bean 的作用域?
Spring 框架支持几种不同的 Bean 作用域,以控制 Bean 的生命周期和访问方式。以下是 Spring 中支持的几种 Bean 作用域:
-
Singleton(单例作用域):这是默认作用域。整个 Spring 容器中只有一个共享的实例。无论多少次请求 Bean,都返回同一个实例。
@Component @Scope("singleton") public class MySingletonBean { // Singleton scoped bean }
2.Prototype(原型作用域):每次请求 Bean 时,都会创建一个新的实例。适用于需要频繁创建新实例的情况。
@Component @Scope("prototype") public class MyPrototypeBean { // Prototype scoped bean }
3.Request(请求作用域):每个 HTTP 请求都会创建一个新的 Bean 实例。仅在 Web 应用中有效。
@Component @Scope("request") public class MyRequestBean { // Request scoped bean }
4.
Session(会话作用域):每个 HTTP 会话都会创建一个新的 Bean 实例。仅在 Web 应用中有效。
@Component @Scope("session") public class MySessionBean { // Session scoped bean }
5.GlobalSession(全局会话作用域):每个全局 HTTP 会话创建一个新的 Bean 实例。主要用于基于 Portlet 的 Web 应用中。
@Component
@Scope("globalSession")
public class MyGlobalSessionBean {
// Global session scoped bean
}
6.Application(应用作用域):在 Spring 4.2 中引入的作用域。它是应用范围的 Bean,只在特定的应用上下文中存在。
Application(应用作用域):在 Spring 4.2 中引入的作用域。它是应用范围的 Bean,只在特定的应用上下文中存在。
spring 自动装配 bean 有哪些方式?
- default:默认的方式和no方式一样
- no:不自动装配,需要使用<ref/>节点或参数
- byName:根据名称进行装配
- byType:根据类型进行装配
- constructor:根据构造函数进行装配
Spring循环依赖解决方法及原理
循环依赖是指两个或多个 Bean 之间相互依赖,形成闭环的情况。Spring 容器默认情况下是不支持循环依赖的,因为这会导致 Bean 的实例化过程无法完成。
循环依赖的原理
Spring 解决循环依赖的原理主要依赖于两阶段注入(Two-Phase Dependency Injection):
Bean 的注册和实例化阶段:
- 当容器加载配置文件或者扫描注解时,会将 Bean 的定义(包括依赖关系)注册到容器中,但是并不立即实例化 Bean。
- 对于循环依赖的 Bean,在第一阶段中,Spring 会提前暴露一个尚未初始化的 ObjectFactory,用于解决循环依赖问题。
Bean 的初始化阶段:
- 在第二阶段中,Spring 容器会对所有 Bean 进行初始化和属性注入。
- 对于循环依赖的情况,Spring 使用三级缓存来解决:
- singletonObjects:用于缓存完全初始化的单例 Bean 实例。
- earlySingletonObjects:用于缓存早期暴露的 Bean 对象,即尚未初始化的 Bean 实例或者正在创建中的 Bean 实例。
- singletonFactories:用于缓存创建 Bean 的工厂,以防止循环依赖导致的递归调用。
解决循环依赖的方法
Spring 提供了几种方法来解决循环依赖问题:
注意事项
通过理解 Spring 的循环依赖解决原理和采用合适的解决方案,可以有效避免因循环依赖而导致的应用程序启动问题和运行时错误。
-
通过构造函数注入:
- 推荐的解决方案是通过构造函数注入来避免循环依赖。Spring 在进行构造函数注入时,可以通过参数传递来满足依赖关系,从而避免循环依赖的问题。
-
通过 @Autowired 注解:
- 使用
@Autowired
注解可以实现字段或者方法的依赖注入。Spring 在实例化 Bean 时会优先注入已经创建好的 Bean 实例,从而在一定程度上避免了循环依赖问题。
- 使用
-
通过 @Lazy 注解:
- 使用
@Lazy
注解延迟初始化 Bean。当 Bean 之间存在循环依赖时,可以使用@Lazy
来标记其中一个 Bean,让 Spring 容器在需要时再去创建 Bean 实例,从而解决循环依赖问题。
- 使用
- 尽量避免双向依赖或者复杂的依赖关系设计,以减少循环依赖的可能性。
- 在解决循环依赖时,需要注意容器的初始化顺序和对象的生命周期,确保依赖关系正确注入。
-
使用 Setter 方法注入:
- 如果无法通过构造函数注入解决循环依赖,可以使用 Setter 方法注入依赖关系。Spring 容器会在对象创建完成后,通过 Setter 方法来注入依赖,从而绕开循环依赖的问题。