Java面试题


序号内容链接地址
1Java面试题https://blog.csdn.net/golove666/article/details/137360180
2JVM面试题 https://blog.csdn.net/golove666/article/details/137245795
3Servlet面试题 https://blog.csdn.net/golove666/article/details/137395779
4Maven面试题 https://blog.csdn.net/golove666/article/details/137365977
5Git面试题https://blog.csdn.net/golove666/article/details/137368870
6Gradle面试题https://blog.csdn.net/golove666/article/details/137368172
7Jenkins 面试题 https://blog.csdn.net/golove666/article/details/137365214
8Tomcat面试题 https://blog.csdn.net/golove666/article/details/137364935
9Docker面试题 https://blog.csdn.net/golove666/article/details/137364760
10多线程面试题 https://blog.csdn.net/golove666/article/details/137357477
11Mybatis面试题 https://blog.csdn.net/golove666/article/details/137351745
12Nginx面试题 https://blog.csdn.net/golove666/article/details/137349465
13Spring面试题 https://blog.csdn.net/golove666/article/details/137334729
14Netty面试题https://blog.csdn.net/golove666/article/details/137263541
15SpringBoot面试题https://blog.csdn.net/golove666/article/details/137192312
16SpringBoot面试题1 https://blog.csdn.net/golove666/article/details/137383473
17Mysql面试题 https://blog.csdn.net/golove666/article/details/137261529
18Redis面试题 https://blog.csdn.net/golove666/article/details/137267922
19PostgreSQL面试题 https://blog.csdn.net/golove666/article/details/137385174
20Memcached面试题 https://blog.csdn.net/golove666/article/details/137384317
21Linux面试题https://blog.csdn.net/golove666/article/details/137384729
22HTML面试题 https://blog.csdn.net/golove666/article/details/137386352
23JavaScript面试题 https://blog.csdn.net/golove666/article/details/137385994
24Vue面试题https://blog.csdn.net/golove666/article/details/137341572
25Ajax面试题https://blog.csdn.net/golove666/article/details/137421929
26Python面试题 https://blog.csdn.net/golove666/article/details/137385635
27Spring Cloud Alibaba面试题 https://blog.csdn.net/golove666/article/details/137372112
28SpringCloud面试题 https://blog.csdn.net/golove666/article/details/137345465
29RabbitMQ面试题 https://blog.csdn.net/golove666/article/details/137344188
30Dubbo面试题 https://blog.csdn.net/golove666/article/details/137346834
31Elasticsearch面试题https://blog.csdn.net/golove666/article/details/137348184
32Oracle面试题https://blog.csdn.net/golove666/article/details/137350452
33Android面试题https://blog.csdn.net/golove666/article/details/137358253
34Kafka面试题 https://blog.csdn.net/golove666/article/details/137358607
35ZooKeeper面试题 https://blog.csdn.net/golove666/article/details/137359255
36Kubernetes面试题 https://blog.csdn.net/golove666/article/details/137365540
37Flink面试题 https://blog.csdn.net/golove666/article/details/137369555
38Hadoop面试题https://blog.csdn.net/golove666/article/details/137370194
39Hive面试题https://blog.csdn.net/golove666/article/details/137371835
40Hbase面试题 https://blog.csdn.net/golove666/article/details/137381853
41Spark面试题https://blog.csdn.net/golove666/article/details/137382815
42Golang面试题 https://blog.csdn.net/golove666/article/details/137395486
43Solr面试题 https://blog.csdn.net/golove666/article/details/137420799

1 Java 基础

1.1 数据类型与变量

在编程语言中,数据类型是一个非常重要的概念,它告诉编译器或解释器该如何处理和存储变量中的数据。数据类型决定了变量的存储需求、布局以及在变量上能进行的操作集合。

Java 语言中的数据类型大致可以分为两类:原始数据类型(Primitive Data Types)和引用数据类型(Reference Data Types)。

原始数据类型

Java 提供了八种原始数据类型,它们直接存储值,而且它们在内存中占用固定的大小:

  1. 整型

    • byte:8位有符号整数,范围从 -128 到 127。
    • short:16位有符号整数,范围从 -32,768 到 32,767。
    • int:32位有符号整数,默认使用这个整型,范围从约 -2.1 亿到 2.1 亿。
    • long:64位有符号整数,大范围整数值。
  2. 浮点型

    • float:32位单精度浮点数。
    • double:64位双精度浮点数,比 float 提供更大的精度。
  3. 字符型

    • char:16位 Unicode 字符,可以存储一个字符。
  4. 布尔型

    • boolean:表示 true/false 值。

引用数据类型

引用数据类型包括类(Class)、接口(Interface)和数组(Array)。它们所引用的对象存储在堆(Heap)内存中,而变量本身存储在栈(Stack)上的引用信息:

  • 类引用:如 String, Integer, Long 等都是引用类型,它们是类的实例。
  • 接口引用:指向实现了接口的类实例的引用。
  • 数组引用:可以是原始数据类型的数组,也可以是对象的数组。

变量

变量是内存中的一个存储区域,用来存储程序运行期间可以变化的数值。在 Java 中声明变量时,必须指定数据类型,还可以为变量指定一个初始值。

变量声明格式通常为:

// 声明一个整型变量
int a;

// 声明一个布尔型变量并初始化
boolean isTrue = true;

// 声明一个字符型变量
char letter = 'A';

// 声明一个浮点型变量
double pi = 3.14159;

注意事项:

  • 引用类型的默认值为 null,而原始数据类型的默认值是它们各自的零值(0, 0.0, false, ‘\u0000’)。
  • 字符串是引用数据类型,但经常被误认为是原始数据类型,因为 Java 提供了特殊的支持和字符串字面量。
  • 数值类型可以进行转换,但需要注意范围大到小的转换可能导致数据丢失。
  • 类型转换可以是隐式的,也可以是显式的(使用强制类型转换)。

1.2 控制流语句

控制流语句是编程语言中用于控制代码执行顺序或分支的结构。基本上,这些语句控制着程序要执行哪些代码块以及在什么条件下执行。以下是大多数编程语言中常见的控制流语句:

1. 条件语句(Conditional Statements)

条件语句用于根据特定条件执行不同的代码块。

  • if 语句: 如果给定条件为 true,执行代码块。

    if (condition) {
        // Code to execute if the condition is true
    }
    
  • if-else 语句: 添加一个备选路径,如果条件为 false,执行这个路径。

    if (condition) {
        // Code to execute if the condition is true
    } else {
        // Code to execute if the condition is false
    }
    
  • else if/elif 语句: 用于检查多个条件。

    if (condition1) {
        // Code if condition1 is true
    } else if (condition2) {
        // Code if condition2 is true
    } else {
        // Code if all conditions are false
    }
    
  • switch/case 语句: 用于基于变量的值来执行多个不同的代码块。

    switch (expression) {
        case value1:
            // Code to run if expression equals value1
            break;
        case value2:
            // Code to run if expression equals value2
            break;
        // ...
        default:
            // Code to run if expression doesn't match any case
    }
    

2. 循环语句(Loop Statements)

循环语句用于重复执行代码块直到满足特定条件。

  • for循环: 具有初始化、条件和迭代步骤的循环。

    for (initialization; condition; increment) {
        // Code to be executed on each loop
    }
    
  • while循环: 只要条件为 true 就一直执行循环。

    while (condition) {
        // Code to be executed as long as the condition is true
    }
    
  • do-while循环: 类似于 while 循环,但保证至少执行一次循环体。

    do {
        // Code block to be executed
    } while (condition);
    
  • foreach/for-in 循环: 用于在集合或数组中循环每个元素。

    for (type item : collection) {
        // Code block to execute
    }
    

3. 跳转语句(Jump Statements)

跳转语句允许程序跳过部分代码或直接跳到特定的点。

  • break语句: 用于立即退出循环。

    break;
    
  • continue语句: 跳过当前迭代,继续执行下一个循环迭代。

    continue;
    
  • return语句: 从当前的函数返回,并可返回一个值。

    return value;
    
  • goto语句: 这是一个标记跳转点的较老语法,现在很多现代编程语言不推荐使用。

    goto label;
    // ...
    label: 
    // Code to jump to
    

控制流语句在所有编程语言中都极其重要,是实现算法逻辑和编写非线性程序的基础。正确使用控制流语句对于提高代码效率、清晰性和易读性至关重要。

1.3 类和对象

在面向对象编程(OOP)中,类和对象是两个核心概念。

类(Class)

类是抽象的蓝图或模板,用于创建具体的对象。它定义了一组属性(也称为字段或成员变量)和方法(也就是函数),这些属性和方法将被它的对象共享。

  • 属性:表示对象的状态,类中定义的变量。
  • 方法:表示可以对对象执行的操作,类中定义的函数。

一个类概念化了现实世界中的实体特性,比如“车”这个类可能包含品牌、型号和颜色等属性,以及启动、停止等方法。

public class Car {
    // 属性
    String brand;
    String model;
    String color;

    // 方法
    void start() {
        // 启动汽车的代码
    }

    void stop() {
        // 停止汽车的代码
    }
}

对象(Object)

对象是类的具体实例。基于类的定义,您可以创建任意数量的对象。

  • 每个对象都有它自己的属性值,这些值定义了对象的状态。
  • 对象可以执行定义在类中的方法

继续使用“车”的例子,基于“车”类,可以创建一个具有品牌“Toyota”,型号“Corolla”,颜色“红色”的汽车对象。

public class Test {
    public static void main(String[] args) {
        // 创建对象
        Car myCar = new Car();

        // 设置对象属性值
        myCar.brand = "Toyota";
        myCar.model = "Corolla";
        myCar.color = "Red";

        // 调用对象的方法
        myCar.start();
        myCar.stop();
    }
}

在这个例子中,myCarCar 类的一个对象。

类与对象的关系

  • 类是对象的蓝图。它定义对象的结构和行为。
  • 对象是类的具体实例。每个对象都是按照类的定义来“构建”的。

面向对象的主要概念

  1. 封装:隐藏对象的内部状态并要求所有的交互通过对象的公共方法进行。
  2. 继承:通过继承创建新类,继承已有类的属性和方法。
  3. 多态性:在运行时,对象可以看作是他们自己的类的实例,也可以看作是他们父类的实例。

总结

类提供了创建对象的详细模版,而对象是这些模板的具体实例。面向对象编程是围绕创建和操作这些类和对象的代码设计和架构的风格。这种编程范式强调数据抽象、封装、模块化、多态和继承。

1.4 继承与多态

在面向对象编程(OOP)中,继承和多态是两个核心的概念,帮助构建灵活、可重用的代码。

继承(Inheritance)

继承是一种使得某一个类(称为子类或派生类)能够继承另一个类(称为基类或父类)属性和方法的机制。这意味着子类除了继承得到父类的特性外,还可以有自己的特性。继承的目的是为了代码复用和实现多态。

在Java或C++这样的语言中,子类使用extends(Java)或:(C++)来声明它们从基类继承。例如,Java中的继承如下所示:

public class BaseClass {
    public void baseMethod() {
        // ...
    }
}

public class SubClass extends BaseClass {
    public void subMethod() {
        // ...
    }
}

在这个例子中,SubClass 继承了 BaseClassSubClass 的实例可以调用 baseMethod(),因为它继承自 BaseClass

多态(Polymorphism)

多态是指允许不同类的对象对同一消息做出响应的能力。换句话说,不同的对象可以通过同一接口接受同一消息,并以各自的方式进行响应。多态背后的思想是我们可以设计方法和类来使用对象的引用或指针,以多种形式表现出来。

在编程中,多态常通过方法重写(overriding,也叫做动态多态)或者方法重载(overloading,也叫做静态多态)来实现。如下是Java中多态的示例:

public class Animal {
    public void sound() {
        System.out.println("Some sound");
    }
}

public class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("Bark");
    }
}

public class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("Meow");
    }
}

在这个例子中,DogCat 类都重写了 sound 方法,根据对象类型(DogCat),同一消息 sound() 会做出不同的响应(Bark或Meow),这就是多态。

重要点

  • 继承提供了一种创建基于现有类来构建新类的方法,提高了代码复用性。
  • 多态能够通过同一个接口处理不同类型的对象,提升了代码的灵活性。
  • 面向对象编程语言如Java, C++, C#和Python等语言都支持继承和多态。

正确使用继承和多态能够提高程序设计的抽象层次和代码的可维护性。

1.5 异常处理

在编程中,异常处理是一种编程结构,用于处理运行时发生的异常情况。异常是编程语言或操作系统在发生错误时抛出的特殊类型的对象或信号,这些错误可能由各种情况引起,如尝试除以零、访问不存在的文件或处理无效输入。

异常处理的核心概念通常包括:

  1. Try

    • 一个尝试块(try block)被用来包裹可能会引发异常的代码。
    • 如果代码执行过程中没有问题,那么之后的异常处理程序(catch blocks)会被跳过。
  2. Catch

    • 如果在 try 块内的代码抛出异常,控制流程会跳转到匹配该异常类型的 catch 块。
    • 每个 catch 块通常指定一个异常类型和一个参数,用于接收抛出的异常对象。
    • catch 块中的代码负责处理异常,比如记录日志信息、清理资源、向用户显示错误信息等。
  3. Finally

    • 无论是否捕获或处理异常,finally 块中的代码都会被执行。
    • 这个块通常被用于清理资源,如关闭文件流或数据库连接等。
  4. Throw

    • throw 关键字用于手动抛出异常。
    • 如果没有被捕获,抛出的异常会导致程序强制终止运行。
  5. Exception Class Hierarchy

    • 大多数编程语言定义了一个异常类层次结构,用以区分不同类型的异常。
    • 自定义异常类可以由更通用的异常类派生出来。

Java 中的异常处理示例

try {
    // 代码块,尝试运行可能引发异常的代码
    int result = 100 / 0; // 这将引发 ArithmeticException
} catch (ArithmeticException e) {
    // 处理算术异常
    System.out.println("不能除以零:" + e.getMessage());
} catch (Exception e) {
    // 可以有多个 catch 块来处理不同类型的异常
} finally {
    // 总是执行的代码块,即使有异常也会执行
    System.out.println("finally 块总是被执行");
}

Python 中的异常处理示例

try:
    # 尝试运行可能引发异常的代码块
    result = 100 / 0  # 这将引发 ZeroDivisionError
except ZeroDivisionError as e:
    # 处理被除数为零的异常
    print("不能除以零:", e)
except Exception as e:
    # 处理除 ZeroDivisionError 外的其他异常
    print("发生了异常:", e)
finally:
    # 总是被执行的代码块,即使有异常也会执行
    print("finally 块总是被执行")

合适的异常处理可以提高程序的健壮性和可读性,并可以帮助开发者更好地理解和调试代码中出现的问题。不过,需要注意的是,过度依赖异常处理也可能导致代码逻辑复杂难懂,因此应当谨慎使用,并在合适的情况下使用异常处理来增强程序的错误处理能力。

2 Java 集合框架

2.1 List、Set 和 Map 接口

在 Java 集合框架中,ListSetMap 是三种不同的接口,它们提供了用于存储数据集合的抽象数据类型。每个接口都有它们的实现类,具有不同的性能特点和用途。

List 接口

  • 有序List 接口实现保持元素的插入顺序,所以可以精确控制列表中每个元素的位置。
  • 索引访问:提供基于索引的访问方式,可以使用索引值来快速访问、搜索和更新元素。
  • 允许重复List 中可以存储重复的元素,即同一个值的元素可以出现多次。

常见的 List 实现类有:

  • ArrayList:基于动态数组实现,提供快速的索引访问和一般较低的内存开销,但其在插入和删除操作上相较于 LinkedList 会慢一些,尤其是在列表中间插入或删除元素时。
  • LinkedList:基于链表实现,允许双向遍历,适用于频繁插入和删除操作的情况。
  • Vector:和 ArrayList 类似,但是线程安全的。通常情况下,由于同步操作的开销,其性能低于 ArrayList

Set 接口

  • 无序:尽管 Set 接口实现存储的元素大多是无序的,但 LinkedHashSet 保留了插入顺序。
  • 唯一性Set 不允许重复元素。每个元素只能存在一次,因此通常用于去重。
  • 不支持索引访问Set 接口的实现没有索引概念,不能使用索引值来直接访问元素。

常见的 Set 实现类有:

  • HashSet:基于哈希表实现,提供快速的查询和存取,是 Set 最快的实现。
  • LinkedHashSet:继承 HashSet,但使用链表维持插入顺序。
  • TreeSet:基于红黑树实现,元素按自然排序或自定义比较器排序,提供有序版本的 Set

Map 接口

  • 键值对Map 存储键值对(key-value pairs),每个键映射到一个值。键是唯一的,而值可以重复。
  • 键的唯一性Map 中的键必须唯一,但对应的值不一定唯一。
  • 不支持索引访问Map 中没有索引概念,但可以通过键来快速访问和修改值。

常见的 Map 实现类有:

  • HashMap:基于哈希表实现,不保证顺序,null 可以作为键或值。
  • LinkedHashMap:继承 HashMap,但使用链表维持键的插入顺序。
  • TreeMap:基于红黑树实现,键按自然顺序或自定义比较器排序,提供有序版本的 Map

总结

ListSetMap 都是 Java 集合框架中的基本接口,专门用于以不同的方式存储和管理数据集合。List 以有序的方式保存元素并允许重复,Set 保证元素唯一性而一般不保证顺序(除了 LinkedHashSet),Map 存储键值对,其键是唯一的。根据具体的应用需求选择合适的接口和实现类,可以优化程序的性能和内存使用。

2.2 集合类的实现和使用

Java 集合框架提供了一组接口和类,用于存储和操作对象组。这些集合类主要分为两大类:一是 Set 和 List,它们都继承自 Collection 接口,用于存储元素的集合;二是 Map,用于存储键值对的映射。

Collection 接口的实现

  1. List 接口

    • ArrayList:基于动态数组实现,提供快速的随机访问能力和顺序访问。但插入和删除元素可能慢,因为需要数组复制或移动元素。
    • LinkedList:基于双向链表实现,提供了更好的插入和删除操作性能,但随机访问较慢。
    • Vector:和 ArrayList 类似,但是它的所有方法都是同步的,是线程安全的。通常情况下,推荐使用 Collections.synchronizedList 方法来获取线程安全的 List 实现,或者使用并发集合类 CopyOnWriteArrayList
    • Stack:继承自 Vector,表示后进先出(LIFO)的栈。
  2. Set 接口

    • HashSet:基于哈希表的 Set 实现,它不保障元素的顺序,允许使用 null 元素。
    • LinkedHashSet:基于哈希表和链表,它保持元素插入的顺序。
    • TreeSet:基于红黑树的 NavigableSet 实现,元素按照其自然排序或者指定比较器排序。

Map 接口的实现

  • HashMap:基于哈希表的 Map 接口实现,存储键值对映射。它不保证映射的顺序。
  • LinkedHashMap:类似于 HashMap,但它保持插入顺序或访问顺序。
  • Hashtable:类似于 HashMap,但它同步且不允许 null 键或 null 值。
  • TreeMap:基于红黑树的 NavigableMap 实现,键值对按自然排序或比较器排序。

特殊集合类和接口

  • Collections:一个包含有关集合操作的静态方法的工具类,如排序、搜索等。
  • Arrays:一个与数组操作相关的工具类,也包括一些静态方法。
  • Queue:用于存储等待被处理的元素群组,包括诸如 LinkedListPriorityQueue 等实现。
  • Deque:双端队列接口,实现类如 ArrayDeque

使用示例

ArrayListHashMap 为例,以下是它们的基本使用方法:

import java.util.*;

public class CollectionExample {
    public static void main(String[] args) {
        // 创建 ArrayList
        List<String> list = new ArrayList<>();
        list.add("Element1");
        list.add("Element2");
        list.remove("Element1");

        // 遍历 List
        for (String elem : list) {
            System.out.println(elem);
        }

        // 创建和使用 HashMap
        Map<String, Integer> map = new HashMap<>();
        map.put("Key1", 10);
        map.put("Key2", 20);

        // 访问 Map
        System.out.println(map.get("Key1"));
        
        // 遍历 Map 的键值对
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

在选择使用哪个集合实现时,应根据需要的数据操作来决定。例如,如果需要频繁的随机存取,ArrayList 是一个好的选择;而需要经常进行插入和删除元素的操作,则可能更倾向于使用 LinkedList。对于键值对映射,则通常选择使用 HashMap。此外,如果需要有序集合或映射,则可以选用 TreeSetTreeMap

2.3 迭代器和迭代过程

迭代器是一种设计模式,用于顺序访问集合(例如数组或列表)中的元素而不需要了解集合内部的底层表示。迭代器提供了一种方法来访问集合的每个元素并进行遍历,同时保护集合免受未经授权的操作。

迭代器的主要特点和功能:

  1. 访问集合的元素:迭代器允许用户逐个访问集合中的元素。

  2. 不公开集合的内部结构:迭代器隐藏了其底层集合的实现细节。

  3. 支持多种遍历方法:可以是前向遍历、后向遍历或者其他复杂的遍历逻辑。

  4. 实现统一的接口:迭代器为不同类型的集合提供了一个公共的接口,允许相同的遍历代码工作在不同类型的集合上。

迭代器的主要方法(以 Java 为例):

  • hasNext():检查集合中是否还有元素。如果有,返回 true;否则返回 false

  • next():返回集合中的下一个元素,并更新迭代器的状态。

  • remove():可选的方法,从集合中移除最近使用 next() 方法返回的元素。

以下是 Java 中使用迭代器的一个简单示例:

List<String> list = Arrays.asList("apple", "banana", "cherry");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String element = iterator.next();
    System.out.println(element);
}

迭代过程

迭代过程是通过迭代器完成的遍历集合的过程。在迭代期间,迭代器维护了当前元素的索引,并在每次迭代中向前或向后移动。这个过程继续进行,直到 hasNext() 返回 false,表示没有更多的元素可以访问。

在某些编程语言中,提供了 “for-each” 循环或相似的构造,用于简化迭代过程。这种语法糖内部使用迭代器来遍历集合,并且对开发者来说隐藏了迭代过程的复杂性。例如,在 Java 中,可以使用以下方式简化迭代过程:

for (String element : list) {
    System.out.println(element);
}

“for-each” 循环内部创建了迭代器实例,并自动处理 hasNext()next() 调用。

迭代器和迭代过程是编程中常用的概念和模式。迭代器使得集合遍历统一且方便,而迭代过程则是使用迭代器来按顺序访问集合元素的活动。

2.4 集合框架的性能特点

Java 集合框架是一组类和接口,它们实现了集合数据结构,如列表、集合和映射。这些类和接口统一了集合的处理方式,提供了一套丰富的接口和丰富的实现方式。每种集合的实现都有各自的性能特点,了解这些性能特点可以帮助开发人员根据具体需求选择最合适的集合类型。

以下是常用集合的性能特点概述:

List 实现

  1. ArrayList

    • 底层数据结构是数组。
    • 访问元素时有更好的性能(get 方法通常是 O(1))。
    • 在列表末尾添加元素性能较好(如果不需要扩容),但在列表中间或开始处添加/删除元素性能较差(因为需要移动元素,O(n))。
    • 随机访问性能良好,但遍历列表的迭代器还是 O(n)。
  2. LinkedList

    • 底层数据结构是双向链表。
    • 在任何位置添加/删除元素的性能较好(O(1)),但需要通过节点顺序访问到特定位置。
    • 访问元素性能较差(O(n)),因为需要从头或尾遍历链表。
    • 在作为栈、队列和双端队列时性能较好。

Set 实现

  1. HashSet

    • 底层数据结构是哈希表。
    • 添加、删除、查找元素通常具有常数时间性能(O(1))。
    • 不保证元素的顺序。
  2. LinkedHashSet

    • 类似于 HashSet,但使用链表维护元素的插入顺序。
    • 性能与 HashSet 相似,但在迭代时会以插入顺序进行。
  3. TreeSet

    • 底层数据结构是红黑树。
    • 保证元素的排序。
    • 添加、删除、查找元素的性能为 O(log n)。

Map 实现

  1. HashMap

    • 底层数据结构是哈希表。
    • 插入和检索键值对通常有很好的性能(O(1))。
    • HashSet 相似,不保证键的顺序。
  2. LinkedHashMap

    • 类似于 HashMap,但使用链表维护键值对的插入顺序。
    • 性能与 HashMap 相似,但迭代顺序和插入顺序一致。
  3. TreeMap

    • 底层数据结构是红黑树。
    • 保证键的排序。
    • 插入、删除和定位键值对的性能为 O(log n)。

其他考虑

  • 初始容量负载因子(HashMap 和 HashSet):适当选择初始容量和负载因子可减少哈希表重哈希操作的频率,从而提高性能。
  • 并发集合(ConcurrentHashMap、CopyOnWriteArrayList 等):在多线程环境下提供线程安全的性能较佳的替代方案。

在选择适当的 Java 集合实现时,考虑数据量、集合的预期大小、操作类型(插入、删除、访问)以及是否需要排序和线程安全是至关重要的。了解每种集合实现的特点,可以帮助你做出合适的决策,从而在特定的使用场景中获得最佳性能。

2.5 并发集合

在Java中,多线程环境下对集合进行操作时常规的集合(如ArrayListHashMap)是不安全的,因此Java提供了一系列线程安全的并发集合类。这些并发集合使用高效的锁机制(如分段锁或无锁CAS操作)来实现线程安全,同时尽量减少同步的开销。

主要的并发集合:

ConcurrentHashMap

ConcurrentHashMap 是一个线程安全的哈希表。它使用锁分段技术,即将数据分为一段一段锁定,而不是对整个表进行锁定,从而提供比 Hashtable 和同步的 HashMap 更高的并发性能。

ConcurrentSkipListMap

ConcurrentSkipListMap 是一个线程安全且按自然顺序排序的映射。内部实现为跳表结构。它通常被用于实现一个可复制和并发的有序映射。

ConcurrentLinkedQueue

ConcurrentLinkedQueue 是一个基于链接节点的并发队列。它使用CAS操作(Compare-And-Swap)来实现线程安全,适用于高并发场景下的队列操作。

LinkedBlockingQueue

LinkedBlockingQueue 是一个基于链接节点的阻塞队列,它在插入和移除时提供了可选的容量限制,确保了线程安全。它适合作为生产者-消费者的队列。

ArrayBlockingQueue

ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,内部使用单一的锁来实现插入和移除的线程安全。

PriorityBlockingQueue

PriorityBlockingQueue 是一个支持优先级排序的无界阻塞队列。每次出队操作都会根据元素的优先级来决定。

CopyOnWriteArrayList

CopyOnWriteArrayList 在写操作时通过复制底层数组来实现线程安全。由于写操作开销较大,它适合读多写少的场景。

CopyOnWriteArraySet

CopyOnWriteArraySet 类似于 CopyOnWriteArrayList,在修改操作时复制底层数组,但是它维护的是一个没有重复元素的集合。

DelayQueue

DelayQueue 是一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时元素才能从队列中取出。

特性

  • 线程安全性:所有操作均为线程安全。
  • 性能:相比于同步的集合类,并发集合提供了更优的性能,尤其在多处理器上。
  • 伸缩性:某些集合(如 ConcurrentHashMap)通过分割锁提供了更好的伸缩性。

使用注意事项

  • 使用这些集合时仍然需要注意迭代器的弱一致性,迭代过程中如果集合发生变化,迭代器并不一定立刻反映这些变化。
  • 对于写操作频繁的场景,比如 CopyOnWriteArrayList,可能会因为复制数组的代价而变得昂贵且性能下降,需要根据实际应用场景仔细选择合适的并发集合。

并发集合在多线程并发情况下对集合进行操作时提供了一个有效且在性能上经过优化的解决方案。

3 Java 输入输出 (I/O)

3.1 输入输出流

在编程领域,输入输出流(I/O 流)是一种读写数据的方式。流(Stream)表示从源(source)读取数据或向目标(destination)写入数据的顺序。使用流可以帮助程序与输入输出设备交互,比如硬盘、网络、内存缓冲区等。

流通常被分为两类:输入流(Input Stream)和输出流(Output Stream)。

输入流(Input Stream)

输入流是用来从数据源读取数据的。在读取操作过程中,程序会通过输入流从数据源(如文件、网络套接字、键盘)中顺序获取数据。程序可以通过输入流一次读取一个字节(Byte)数据,也可以读取一个包含多个字节的数据块。

在 Java 中,InputStream 是表示字节输入流的所有类的超类。一些常用的输入流类包括:

  • FileInputStream:用于从文件中读取数据。
  • ByteArrayInputStream:可以将字节数组作为数据来源。
  • BufferedReader:用于从字符输入流中高效地读取文本数据。

输出流(Output Stream)

输出流是用来向数据目标写入数据的。在写入操作过程中,程序会使用输出流把数据以顺序的方式发送到目的地(如文件、网络套接字、显示器)。和输入流类似,输出流可以一次写入一个字节,也可以写入一个字节数据块。

在 Java 中,OutputStream 是表示字节输出流的所有类的超类。一些常用的输出流类包括:

  • FileOutputStream:用于将数据写入文件。
  • ByteArrayOutputStream:可以将数据写入字节数组。
  • BufferedWriter:用于向字符输出流中高效地写入文本数据。

流的分类

根据处理的数据单位,流可以分为字节流和字符流:

  • 字节流(Byte Streams):用于处理数据流的单个字节,如音频、图片等二进制文件。
  • 字符流(Character Streams):用于处理字符数据,比如读写文本文件。

示例(Java 中的输入输出流)

以下是一个 Java 程序中使用 FileInputStreamFileOutputStream 读写文件的简单例子:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopyExample {
    public static void main(String[] args) {
        try (FileInputStream in = new FileInputStream("source.txt");
             FileOutputStream out = new FileOutputStream("destination.txt")) {
            int c;
            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } catch (IOException e) {
            System.out.println("IOException: " + e.getMessage());
        }
    }
}

在上面的例子中,我们使用 FileInputStream 从文件 source.txt 中按字节读取数据,并使用 FileOutputStream 将读取的数据写入文件 destination.txt 中。

注意

在使用输入输出流时,确保流在使用完毕后被关闭是非常重要的。不关闭流可能会导致资源泄露,如打开文件描述符未释放。在 Java 7 以上版本中可以使用 try-with-resources 语句,它可以确保在 try 语句执行完毕后自动关闭相关资源。

3.2 文件读写

在编程中,文件读写是基本的 I/O(输入/输出)操作,允许你从文件中读取数据以及向文件中写入数据。这些操作在很多编程语言中都有支持,包括但不限于 Java、Python、C++、C#等。以下是不同编程语言中文件读写操作的基础知识。

Java 中的文件读写

Java 提供了多种类和方法用于文件读写,如 FileReaderBufferedReaderFileWriterBufferedWriter。以下是使用这些类进行文件读写的基本示例:

读取文件:

try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        // 处理每一行
    }
} catch (IOException e) {
    e.printStackTrace();
}

写入文件:

try (BufferedWriter bw = new BufferedWriter(new FileWriter("file.txt"))) {
    bw.write("Some text");
    bw.newLine();
    // 写入更多内容
} catch (IOException e) {
    e.printStackTrace();
}

Python 中的文件读写

Python 提供了 open 函数用于打开文件,它可以返回一个文件对象,随后便可以在这个文件对象上调用 .read().readlines().write() 等方法。使用 with 语句可以自动管理文件的打开和关闭。

读取文件:

with open('file.txt', 'r') as file:
    for line in file:
        # 处理每一行

写入文件:

with open('file.txt', 'w') as file:
    file.write("Some text\n")
    # 写入更多内容

C++ 中的文件读写

在 C++ 里,fstream 库提供了 ifstreamofstream 类用于文件读写。

读取文件:

#include <fstream>
#include <iostream>
#include <string>

std::ifstream file("file.txt");
std::string line;
while (std::getline(file, line)) {
    // 处理每一行
}
file.close();

写入文件:

#include <fstream>
#include <iostream>

std::ofstream file("file.txt");
if (file.is_open()) {
    file << "Some text\n";
    // 写入更多内容
    file.close();
}

C# 中的文件读写

在 C# 中,可以使用 System.IO 命名空间中提供的类,例如 StreamReaderStreamWriter

读取文件:

using (StreamReader sr = new StreamReader("file.txt")) {
    string line;
    while ((line = sr.ReadLine()) != null) {
        // 处理每一行
    }
}

写入文件:

using (StreamWriter sw = new StreamWriter("file.txt")) {
    sw.WriteLine("Some text");
    // 写入更多内容
}

注意事项

  • 总是确保文件在操作完成后被正确关闭,以避免资源泄漏。在支持自动管理资源的语言中(例如使用 Python 的 with 语句或 Java 的 try-with-resources),应尽可能使用这些特性。
  • 处理文件读写时应捕获并处理可能发生的异常,例如文件未找到、没有读写权限等。
  • 写入操作默认会覆盖文件内容,除非特别指定追加模式,例如在 Python 中使用 'a' 模式,或在 Java 的 FileWriter 构造函数中传递 true 值。
  • 在读取大文件时,应使用缓冲或分批读取的方式,以减少内存消耗。

文件读写是大多数软件应用的基本需求,无论是数据存储、配置管理还是用户交互。掌握如何在各种编程语言中进行文件读写操作是成为一个有效的软件开发者的重要技能之一。

3.3 序列化机制

序列化是一种将对象的状态信息转换成可以存储或传输的形式的过程。在 Java 中,序列化机制允许将实现了 java.io.Serializable 接口的对象转换成一个字节流,并能在之后将这个字节流完整地恢复成原来的对象。这个过程在远程通信和对象持久化时非常有用。

实现 Java 序列化的步骤

  1. 声明对象可序列化
    类必须实现 Serializable 接口,这个接口是一个标记接口,没有方法需要实现。

    public class User implements Serializable {
        private static final long serialVersionUID = 1L; // 版本控制
        private String name;
        private transient String password; // 'transient' 关键字表示该属性不会被序列化
        // getters 和 setters 省略...
    }
    
  2. 对象序列化
    使用 ObjectOutputStream 类将对象转换成字节流。

    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
        User user = new User();
        user.setName("John Doe");
        user.setPassword("s3cr3t");
        oos.writeObject(user);
    }
    

    使用 ObjectOutputStreamFileOutputStream 的组合可以将对象状态保存到文件中。

  3. 对象反序列化
    使用 ObjectInputStream 类从字节流中恢复对象。

    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
        User user = (User) ois.readObject();
        // use the user object...
    }
    

    使用 ObjectInputStreamFileInputStream 的组合可以从文件中读取对象状态。

序列化的关键点

  • serialVersionUID:是用来表明类的不同版本间的兼容性。如果类的 serialVersionUID 有变化,反序列化时可能会抛出 InvalidClassException。所以建议显示声明 serialVersionUID 值。
  • transient 关键字:可以阻止字段被序列化到文件中,防止敏感信息如密码等被暴露。
  • 注意序列化的类的变动:如果修改了类的定义,那么它序列化的形式也可能会改变,和旧版本的序列化形式不兼容。

使用场景

  • 永久性存储:将对象的状态保存到一个存储媒介中,以便将来可以再次读取。
  • 对象深拷贝:复制对象时,对象图内所有对象都会被复制。
  • 跨 JVM 的通信:在客户端和服务器间传输对象的状态。

序列化的替代方案

序列化的实现不仅限于 Java 内置的方式,也可以使用如 JSON、XML、Protobuf 等数据格式通过第三方库进行序列化,这在现代的应用程序中变得越来越普及。

注意事项

序列化虽然为对象存储和传输提供了方便,但在使用过程中需要考虑安全性和版本控制问题,因为序列化数据可能会被恶意篡改,或者在各种版本的类之间反序列化可能会失败。此外,序列化和反序列化也可能是一个耗时的过程,对性能有影响。

3.4 NIO 和 NIO.2

NIO(New Input/Output)是 Java 中的一个高级 I/O API,用来支持基于缓冲区的 I/O 操作,非阻塞 I/O,以及通道(Channel)等。NIO 是在 Java 1.4 版本中引入的,作为标准的 I/O 流(例如 InputStream 和 OutputStream)的替代和补充,它提供了更为高效的文件和网络数据处理。

NIO 主要特性:

  • 缓冲区(Buffer):NIO 通过缓冲区作为数据的临时存储方法,用于读取和写入数据。缓冲区是固定大小的对象,可以帮助批量处理数据,提高 I/O 效率。
  • 通道(Channel):类似于流(Stream),但通道是双向的,既可以从通道中读取数据,也可以写入数据到通道。
  • 选择器(Selector):允许单个线程同时监控多个通道的 I/O 状态,实现非阻塞 I/O 操作,特别是在需要管理数百个网络连接时非常有用。

NIO 的应用场景:

  • 构建高性能的网络服务器或客户端
  • 需要处理大文件,且希望避免 Java 老式 I/O 提供的同步阻塞调用

NIO.2(也称为 JSR 203 或“New I/O 2”)

NIO.2 是在 Java 7 版本中引入的一组 I/O API 的扩展,提供了对文件系统访问、文件事件通知和改善的异步文件 I/O 性能的支持。

NIO.2 提供的关键特性:

  • 文件系统 API:提供了一个与文件系统交互的全面接口,如遍历文件树、使用符号链接等。
  • 异步文件 I/O 操作:用于提供非阻塞的文件 I/O 操作,可以提升程序在处理文件时的响应能力。
  • 文件更改通知(Watch Service):允许应用程序监控文件或目录的变化,例如可以监控文件的创建、删除或修改。
  • 更强的文件属性支持:提供了对文件和文件元数据(如权限、所有者信息等)更细粒度控制的能力。

NIO.2 主要的类和接口包括 PathPathsFilesFileSystemFileChannelAsynchronousFileChannel 等。

通过使用 NIO 和 NIO.2,Java 开发者可以构建更为强大和高效的 I/O 处理功能。这些都是现代 Java 应用程序中实现文件操作和网络编程的关键技术。

4 Java 并发编程

4.1 线程的创建和管理

在 Java 中,线程是并发执行的程序单元。创建和管理线程是 Java 多线程编程的重要部分。以下是在 Java 中创建和管理线程的基础知识。

创建线程

有两种常见的方式来创建线程:

  1. 通过继承 Thread 类
    创建一个继承自 Thread 类的新类,然后重写 run() 方法。

    public class MyThread extends Thread {
        @Override
        public void run() {
            // Code that executes on the new thread
        }
    }
    
    // 创建并启动线程
    MyThread thread = new MyThread();
    thread.start();
    
  2. 通过实现 Runnable 接口
    创建一个实现了 Runnable 接口的类,实现 run() 方法。

    public class MyTask implements Runnable {
        @Override
        public void run() {
            // Code that executes on the new thread
        }
    }
    
    // 创建并启动线程
    Thread thread = new Thread(new MyTask());
    thread.start();
    

管理线程

一旦线程被创建和启动,你可能需要管理它的执行和生命周期。这涉及到线程的几个方面:

  • 线程的生命周期
    线程的状态可以是新建(New)、就绪(Runnable)、执行(Running)、阻塞(Blocked)、等待(Waiting)、定时等待(Timed Waiting)和终止(Terminated)。

  • 线程优先级
    通过 setPriority() 方法设置,线程优先级决定了当有多个线程就绪时,哪个线程最有可能得到执行。

  • 让步(Yielding)
    Thread.yield() 方法使当前运行的线程回到就绪状态,以允许具有相同优先级的其他线程获得执行。

  • 睡眠(Sleeping)
    Thread.sleep(long milliseconds) 方法使当前执行的线程暂停执行指定的时间(给其他线程执行的机会)。

  • 加入(Joining)
    thread.join() 方法调用将使得一个线程等待另一个线程完成才继续执行。

  • 中断(Interrupting)
    thread.interrupt() 方法可用于请求终止线程。线程可以通过检查中断状态来响应中断。

  • 同步(Synchronization)
    关键字 synchronized 用于控制对共享资源的访问,以防止出现并发问题。

  • Daemon 线程
    通过调用 Thread.setDaemon(true) 将线程设置为守护线程(Daemon),这表明这个线程不应阻止程序关闭。

线程池

对于管理大量线程的复杂应用,可以使用线程池 ExecutorService。线程池管理一组线程,并通过工作队列来分配任务。

ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(new MyTask());

当使用完线程池后,应调用 executor.shutdown() 方法来关闭线程池。

注意事项

  • 确保线程的 run() 方法不会执行无限循环或不必要的阻塞操作。
  • 避免在创建大量线程,这可能导致内存溢出或过多的上下文切换影响性能。
  • 使用已有的并发工具类(如 ExecutorServiceCountDownLatchCyclicBarrierSemaphore)来管理复杂的线程交互。

正确地创建和管理线程对于编写高效、无错误的多线程程序至关重要。

4.2 同步机制

在多线程编程环境中,同步机制是一种确保两个或多个并发进程或线程在执行某段特定代码部分不会发生冲突的技术。同步是必要的,因为它可以防止多个线程同时访问共享资源(比如数据结构、文件等)而导致的数据不一致问题。

以下是一些主要的同步机制:

1. 锁(Locks)

锁提供了一种排他机制,确保同时只有一个线程能够执行特定的代码块。在Java中,锁的实现有很多,包括:

  • 内建锁(Synchronized方法或块)
    在方法或代码块上使用syncronized声明来获取对象的内部锁。

  • 显式锁(ReentrantLock)
    java.util.concurrent.locks包提供了显式锁,例如ReentrantLock,允许对比syncronized更灵活的锁操作。

2. 信号量(Semaphores)

信号量用于限制可以访问某个资源的线程数。它主要用于实现资源池或给代码访问进行限流。

3. 监视器(Monitor)

监视器是一种同步构造,它将对象的字段和对象的方法打包成一个单独的复合结构。当方法在对象上同步时,它们按需对内置锁进行获取和释放。在Java中,任何使用了synchronized关键字的对象都是一个监视器。

4. 计数器闭锁(CountDownLatch)

CountDownLatch是一种同步辅助类,在完成一组正在其他线程中执行的操作之前,它可以使一个或多个线程等待。

5. 栏(Barrier)

栏类似于闭锁,它阻塞一组线程直到某个事件发生。CyclicBarrier是一个同步辅助类,允许一组线程所有都达到某个条件才能继续执行。

6. 读/写锁(ReadWriteLocks)

读写锁允许同时有多个读访问和单个写访问。在java.util.concurrent.locksReadWriteLock实现了这种机制。

7. 原子变量(Atomic Variables)

java.util.concurrent.atomic包提供了一组原子变量,如AtomicIntegerAtomicReference,这些类利用底层并发特性来实现同步,通常用于计数器或累加器。

8. 条件(Conditions)

条件为线程提供了一种暂停执行(await)的方法,直到某个条件为真。条件通常与锁一起使用。在Java中,ReentrantLock对象可以配合Condition实例一起实现复杂的线程同步。

注意事项

  • 正确使用同步机制需要充分理解它们的工作原理以避免死锁和活锁等问题。
  • 过度同步可能导致性能问题,因此应当尽量减少同步块的范围,在不影响安全性的前提下使其尽可能的短。
  • 应当选择适合所面临问题的最简单同步机制。

以上各种同步机制可根据具体场景和需求选择使用。在选择时,需要平衡简单性、性能以及对问题的适配程度。

4.3 线程池

线程池是一种基于池化技术的多线程管理方式,用于减少在创建和销毁线程上所花费的开销和资源使用。线程池中的线程可以循环使用,来并发执行多个任务。

核心概念

  1. 线程池管理器(Thread Pool Manager):
    管理线程池的创建、销毁和属性设置等。

  2. 工作线程(Worker Threads):
    线程池中的线程,用于实际执行任务。

  3. 任务队列(Task Queue):
    存储待处理任务的队列,用于保存尚未开始的任务。

  4. 任务接口(Task Interface):
    用于定义具体的任务,比如 Runnable 或 Callable 接口的实现。

  5. 线程池大小(Pool Size):
    池中同时活跃的线程数量。

线程池的优势

  1. 减少资源消耗
    重复利用已创建的线程,避免频繁创建和销毁线程造成的资源浪费。

  2. 提高响应速度
    当任务到来时,任务可以不需要等待线程创建就立即执行。

  3. 方便线程管理
    线程池可以统一分配、调优和监控线程。

  4. 提高系统稳定性
    可以通过限制队列大小来防止因大量并发线程导致的内存消耗过多。

Java中的线程池

Java 中的 java.util.concurrent 包提供了线程池的实现,其中 Executor 框架是用来创建和管理线程池的。最常用的线程池管理器是 ThreadPoolExecutor 类和 Executors 类。

Executors 类中预定义的线程池:

  • newFixedThreadPool
    创建一个固定大小的线程池。

  • newCachedThreadPool
    创建一个可根据需要创建新线程的线程池。

  • newSingleThreadExecutor
    创建一个只有一个线程的线程池。

  • newScheduledThreadPool
    创建一个可以延时或定期执行任务的线程池。

配置 ThreadPoolExecutor:

int corePoolSize = 10;
int maximumPoolSize = 20;
long keepAliveTime = 60;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    unit,
    workQueue
);

在上面的代码中,corePoolSize 是线程池的基本大小,maximumPoolSize 是线程池的最大线程数,keepAliveTime 是非核心线程的闲置超时时间,当闲置时间达到 keepAliveTime 指定的值后,非核心线程会被回收。

使用线程池执行任务:

executor.execute(new Runnable() {
    @Override
    public void run() {
        // 任务内容
    }
});

或者,使用 Future 获取任务执行结果:

Future<?> future = executor.submit(new Callable<Object>() {
    @Override
    public Object call() {
        // 任务内容,可返回结果
        return null;
    }
});

关闭线程池:

调用 shutdown() 来完成所有已提交的任务后关闭线程池;或者调用 shutdownNow() 以尝试立即关闭线程池并尽快停止所有正在执行的任务。

线程池在处理多线程应用程序,特别是在需要处理大量短生命周期异步任务的程序中,是一种非常有用并且广泛推荐使用的设计模式。

4.4 并发集合和类

在 Java 中处理并发的集合通常意味着在多个线程访问和修改同一个集合时,考虑线程安全和性能优化。Java 并发 API 提供了一系列线程安全的集合类,它们可以在并发环境中使用,而不会引发竞态条件或数据不一致。

并发集合类概述

以下是一些 Java 中的并发集合类和接口:

1. ConcurrentHashMap

  • 一个线程安全的 HashMap 实现。
  • 使用分段锁(segmented locks)机制提供更高的并发性能。
  • 支持完整的 Map 接口,并允许同时读取和写入操作。

2. CopyOnWriteArrayList

  • 一个线程安全的 List 实现。
  • 在每次修改时都会创建底层数组的一个新副本,适用于读多写少的场景。
  • 对列表进行迭代时不会抛出 ConcurrentModificationException。

3. CopyOnWriteArraySet

  • 一个线程安全的 Set 实现。
  • 类似于 CopyOnWriteArrayList,它基于数组并在每次修改时复制数据。

4. ConcurrentLinkedQueue

  • 一个线程安全的无界队列实现。
  • 使用非阻塞算法保证线程安全,适用于高并发场景。

5. BlockingQueue 接口

  • 一个支持阻塞操作的队列接口。
  • 常用实现有 ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue
  • 适用于生产者-消费者模型,支持生产者或消费者在必要时阻塞等待。

6. ConcurrentSkipListMap 和 ConcurrentSkipListSet

  • 分别对应 SortedMapSortedSet 接口的并发实现。
  • 使用跳跃列表(Skip list)数据结构来提供日志时间复杂度的排序操作。
  • 支持自然排序或自定义 Comparator

7. ConcurrentNavigableMap 接口

  • 扩展了 NavigableMap 并增加了并发操作支持。
  • ConcurrentSkipListMap 是其唯一实现。

使用并发集合类的好处

  • 提供了比使用 Collections.synchronizedMap()Collections.synchronizedList() 等旧方法更细粒度的锁定,更高的并发性能。
  • 减少了使用显式同步(如 synchronized 关键字)的需要。
  • 扩展了标准集合接口,允许在不改变代码结构的情况下对并发应用进行优化。
  • 为设计并发应用提供了丰富的数据结构,既安全又有效。

注意事项

  • 并发集合通常有额外的内存和性能开销,仅在多线程访问时才使用它们以实现最佳性能。
  • 考虑使用的场景。例如,在读多写少的场景中,CopyOnWriteArrayList 可能是个好选择;在需要高并发写操作时,ConcurrentHashMap 更合适。
  • 注意线程安全的集合类不会阻止所有并发问题,如原子性操作仍需要其他并发控制手段,比如原子变量等。

并发集合类是 Java 中处理线程安全集合的解决方案,并且它们在大多数多线程程序中是不可或缺的。适当地使用这些类可确保数据结构在并发访问下保持正确性,同时提升应用程序的性能。

4.5 Java 内存模型和锁优化

Java 内存模型 (JMM)

Java 内存模型(JMM)是一个抽象的概念,它描述了在并发环境下如何以及何时可以看到其他线程对共享变量的写入。JMM 定义了线程和主内存之间的抽象关系和规则,旨在解决可见性、原子性以及有序性等问题。

JMM 的关键概念包括:

  • 工作内存和主内存:所有变量都存储在主内存中,每个线程都有自己的工作内存,工作内存包含了主内存变量的拷贝。
  • 内存屏障:一种 CPU 指令,用于控制特定操作的执行顺序,防止编译器和处理器的重排序。
  • :提供互斥访问,确保只有获得锁的线程可以访问特定资源。
  • volatile 变量:确保对变量的写入能立即反映到主内存,对变量的读取能直接从主内存访问,从而保证了变量的可见性。

锁优化

为了减少线程同步的开销,Java 平台实现了许多锁优化技巧,主要目的是提高多线程程序的执行效率。

一些常见的锁优化策略和技术包括:

  • 轻量级锁:在没有真正的竞争情况下,将对象头上的锁标志位通过 CAS 操作来申请锁,避免了生成 OS 层级的线程阻塞。
  • 偏向锁:针对于一个锁大部分时间仅被同一个线程访问的场景,通过偏向这个线程,避免了不必要的锁竞争。
  • 锁粗化:如果连续的几个操作都对同一个对象加锁,就可以将锁的同步范围扩展到整个操作序列的外部。
  • 锁消除:编译器优化的过程中,对那些不可能共享资源的锁请求进行消除。
  • 可重入锁 (ReentrantLock):用于代替 synchronized,在特定情况下提供更好的性能和功能。

JMM 与锁优化的目的

JMM 以及这些锁优化技术的目的是降低并发编程的复杂度,同时尽可能地提高并发性能。通过理解 JMM,程序员可以编写出更安全、更高效的并发代码,而 JVM 和 JIT 编译器提供的各种锁优化技术,可以保证并发程序在现代多核处理器上运行得更快。

注意事项

虽然锁优化能够提高性能,但它们也可能带来新的问题,比如降低了代码的可预测性,增加了调试和性能分析的复杂度。因此,在设计并发程序时,遵循最佳实践和编码原则非常重要。此外,在对程序进行优化前,建议进行充分的性能测试和评估。

5 Java 虚拟机 (JVM)

5.1 JVM 的内存区域

Java虚拟机(JVM)的内存区域划分主要是为了优化Java程序的运行效率和稳定性。JVM的运行时内存主要分为以下几个区域:

方法区(Method Area)

方法区是所有线程共享的内存区域,用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

堆(Heap)

堆也是一个所有线程共享的内存区域,主要用于存放对象实例和数组。它是垃圾收集器进行垃圾回收的主要区域,因此又分为新生代(Young Generation)和老年代(Old Generation)。

Java栈(Java Stack)

Java栈是线程私有的,它的生命周期与线程相同。每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在Java栈中入栈到出栈的过程。

本地方法栈(Native Method Stack)

本地方法栈与Java栈作用类似,区别在于本地方法栈服务于本地方法。本地方法是使用诸如C、C++等语言编写的方法。

程序计数器(Program Counter)

程序计数器是线程私有的,当前线程所执行的字节码的行号指示器。字节码解释器通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

直接内存(Direct Memory)

直接内存不是JVM运行时数据区的一部分,它并不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。它是在Java堆外直接向系统申请的内存区域。Java NIO类是典型的使用直接缓冲区的例子,可以在某些场景中提高性能。

在Java虚拟机中,可能还会有一些其他小的辅助存储区域,如对性能调优有帮助的代码缓存区域。不同版本和不同实现的JVM可能在内存管理上会有所不同,但基本的区域划分和原理是相似的。

了解这些内存区域对于优化Java程序、垃圾收集(GC)策略和解决内存泄漏问题非常关键。

5.2 垃圾收集器与垃圾收集算法

在 Java 中,垃圾收集(Garbage Collection,GC)是 JVM 的一个重要功能,它自动管理程序分配的内存。当对象不再被使用时,GC 会清理这些对象占用的内存空间。垃圾收集器是实现这一过程的工具,而垃圾收集算法则是垃圾收集器背后的理论基础。

垃圾收集器(Garbage Collectors)

Java 提供了几种不同的垃圾收集器,每种都有其自身的特点,适合不同的场景和需求:

  1. Serial Garbage Collector

    • 单线程收集器,适用于单核处理器或小内存环境。
    • 使用“标记-清除-整理”(Mark-Sweep-Compact)算法。
  2. Parallel Garbage Collector (也称为 Throughput Collector):

    • 多线程收集器,关注吞吐量,适用于多核服务器。
    • 默认在新生代使用“标记-复制”(Mark-Copy),在老生代使用“标记-清除-整理”算法。
  3. Concurrent Mark Sweep (CMS) Collector

    • 以最小化应用程序停顿为目标的多线程收集器。
    • 主要使用“标记-清除”(Mark-Sweep)算法。
  4. G1 Garbage Collector

    • 旨在替代 CMS,适用于大堆内存和多核服务器。
    • 分割成多个区域(Region)并行处理,使用“标记-整理”(Mark-Compact)算法。
  5. Z Garbage Collector (ZGC)Shenandoah

    • 针对低延迟的垃圾收集器。它们是实验性质的,旨在实现几乎没有停顿时间的 GC。
    • 使用负载均衡和增量的垃圾收集技术。

垃圾收集算法(Garbage Collection Algorithms)

垃圾收集算法描述了垃圾收集器如何识别无用的对象,以及如何回收它们占用的内存。

  1. 标记-清除(Mark-Sweep)

    • 算法分为“标记”和“清除”两个阶段:标记出所有从根集合可达的对象,然后清除那些未被标记的对象。
  2. 标记-复制(Mark-Copy)

    • 主要用于新生代。将内存分为两块,每次只用一块。在进行垃圾收集时,将正在使用的内存中的活动对象复制到未被使用的内存块,然后清空正在使用的内存块。
  3. 标记-清除-整理(Mark-Sweep-Compact)

    • 是标记-清除算法的改进版本,增加了碎片整理的步骤,将存活对象向内存的一端移动,以便为新对象提供连续的空间。
  4. Stop-and-Copy

    • 该算法类似于标记-复制,但是它会停止所有的应用程序线程,直到复制工作完成。
  5. 增量收集(Incremental Collection)

    • 将垃圾收集的工作分成多个小步骤进行,减少每次垃圾收集时应用暂停的时间。
  6. 分代收集(Generational Collection)

    • 这种方法基于这样的观察:不同年龄的对象的死亡率不同。JVM 将对象分为几代,通常分为新生代、老生代(有时还包括永久代或元数据区),并根据对象的寿命采取最适合的收集策略。

每种算法都有其优缺点,通常,GC算法的选择和调整必须根据具体的应用程序性能需求和目标来完成。优化 GC 和减少 GC 延迟可以通过 JVM 启动参数、监控 GC 行为以及可能的代码更改来实现。垃圾收集器的选择和调整对于优化 Java 应用程序的性能非常关键。

5.3 类加载机制

在Java中,类加载机制涉及查找、加载、链接(验证、准备和解析),以及初始化类的过程。Java 虚拟机 (JVM) 使用类加载器(Class Loaders)来动态加载 Java 类到运行时数据区。当程序提到一个类时,JVM 会委托类加载器按需加载此类。

类加载器

Java中的类加载器包括:

  1. 引导类加载器(Bootstrap Class Loader)
    这是虚拟机的内置类加载器,它加载核心Java API(在JDK的<JAVA_HOME>/jre/lib/rt.jar里的类)。

  2. 扩展类加载器(Extension Class Loader)
    它加载JDK扩展API(在JDK的<JAVA_HOME>/jre/lib/ext目录或者由系统属性java.ext.dirs指定的路径中的类)。

  3. 系统(应用)类加载器(System/Application Class Loader)
    它根据Java应用的类路径(CLASSPATH)加载应用程序的类。

  4. 用户自定义类加载器
    可以通过继承java.lang.ClassLoader类来创建自定义类加载器,以控制字节码加载的方式和时机。

类加载的过程

类加载的过程包括以下阶段:

  1. 加载
    类加载器会读取类的全名,并将类的二进制数据加载到JVM中。此时,加载的类处于原生态(原始的、未链接的)。

  2. 链接

    • 验证:检查加载的类是否符合JVM规范,如是否有正确的内部结构和字节码。
    • 准备:为类的静态变量分配内存,并设置默认初始值。
    • 解析:将类的二进制数据中的符号引用转换为直接引用。
  3. 初始化
    JVM执行类的构造器方法的代码,这个方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。在初始化阶段,依据程序员通过程序编码指定的主观计划去初始化类变量和其他资源或执行其他初始化动作。

类加载的时机

在以下情况下,JVM会开始加载类:

  1. 创建类的实例时。
  2. 访问某个类或接口的静态方法,或者使用静态字段时(读取或设置)。
  3. 使用java.lang.reflect包的方法对类进行反射调用时。
  4. 初始化子类时,需要先初始化其父类。
  5. JVM启动时,用户指定的主类(包含main方法的类)将被加载。

类加载器的特性

  • 委派模型
    类加载器在尝试加载类时,首先会委托给父类加载器进行加载,直到引导类加载器,这能确保Java核心库的类型安全。

  • 可见性原则
    子类加载器可以访问父类加载器加载的类,而父类加载器不能访问子类加载器加载的类。

  • 单一性原则
    由于委派机制和缓存机制,每个类加载器只会加载一次类。不同的类加载器可以加载相同名称的类,但仍然认为是不同的类。

了解和理解类加载机制对于Java开发者来说非常重要,它对挖掘类加载过程中的问题、自定义类加载器以及应用程序的模块化有着深远的影响。

5.4 JVM 性能调优

Java虚拟机(JVM)的性能调优是一个复杂的过程,需要对JVM的工作原理有深刻的理解,尤其是内存管理和垃圾回收机制。寻找最佳性能设置通常涉及到监控、分析性能数据、调整JVM选项并不断测试。以下是性能调优的一些通用步骤和策略:

1. 监控 JVM 性能

首先,要使用工具监控JVM的科学指标,这些指标可能包括:

  • 堆内存使用情况(Heap memory usage)
  • 垃圾收集器的性能(Garbage collector performance)
  • CPU和内存的使用情况
  • 线程锁等待与阻塞(Thread lock waits and blocking)

性能监控工具如 VisualVM、JConsole、Grafana with a JVM monitoring plugin、New Relic、AppDynamics 等。

2. 垃圾收集(GC)调优

GC调优可能是提升JVM性能最有效的方法之一,核心目标是减少GC暂停时间并降低延迟,调优步骤包括:

  • 选择合适的垃圾收集器:G1、CMS(并发标记清除)、ParallelGC、ZGC、Shenandoah 等。
  • 调整堆内存大小(-Xms 和 -Xmx)。
  • 设置新生代和老年代的大小。
  • 配置GC日志和了解上面提到的GC日志信息。

3. 堆内存和堆外内存调优

合理分配堆内存(Heap)和非堆内存(Non-Heap 或 Native memory),关注Direct Memory和Mapped Buffers的使用,以及它们在系统的整体性能中的作用。

4. JVM参数调整

深入理解和正确使用JVM参数对性能进行微调:

  • -Xmx-Xms 控制最大堆大小和初始堆大小
  • -XX:+UseG1GC 使用G1垃圾收集器
  • -XX:MaxGCPauseMillis 设置目标GC暂停时间
  • -verbose:gc-XX:+PrintGCDetails 打开GC日志
  • -XX:+UseStringDeduplication 在Java 8u20及以后使用,用于减少堆中的字符串数量。

5. Java程序优化

与JVM调优一样重要的是Java代码的性能优化:

  • 使用合适的数据结构和算法来减少资源消耗。
  • 避免内存泄漏,如及时清理无用对象引用。
  • 减少对象创建,特别是大对象或生命周期长的对象。
  • 减少JNI调用,因为它们可能会引起额外的开销。
  • 多线程程序需要合理管理同步和并发。

6. 应用层面的性能考虑

应用级的优化包括数据库的查询优化、使用缓存、异步处理和调整线程池参数等策略。

7. 持续调优

JVM性能优化是一个持续的过程,需要定期执行基准测试和A/B测试,并监控生产环境中的表现,以便及时响应潜在的性能问题。

性能调优的最终目标是找到符合应用需求的平衡点,既确保了高性能,又不过分消耗资源。每个Java应用和环境都是独特的,因此不存在一刀切的解决方案,每项优化都需要衡量其实际效果。

5.5 Java 自动内存管理

Java 自动内存管理是一个重要的特性,它使得开发者无需直接控制内存分配和回收。这是通过垃圾回收(Garbage Collection, GC)机制实现的,GC 自动将程序不再需要的对象标记为垃圾,并在合适的时候回收其内存空间。

垃圾收集机制

Java 虚拟机(JVM)内的垃圾收集器会周期性地查找和回收未使用的对象,以释放资源。这个过程分为几个阶段:

  1. 标记(Marking):GC 标记所有从根集(root set)可达的对象。根集是一组引用,包括静态变量引用、活动线程中的本地变量引用等。那些没有被标记的对象可以视为垃圾。

  2. 清除(Sweeping)/压缩(Compaction):GC 删除那些未被标记的对象,回收它们占用的内存空间。在压缩阶段,GC 会移动对象来消除内存中的碎片。

  3. 回收(Collection):GC 会回收这些空间,使它们可以被程序用来创建新的对象。

垃圾收集器的类型

JVM 提供了多种类型的垃圾收集器,每种都有其特点和适用场景:

  • 串行收集器(Serial GC):对单线程环境进行优化,通常适用于小型应用和简单工作负载。
  • 并行收集器(Parallel GC):也称为吞吐量收集器,它使用多个线程同时进行垃圾回收,适用于多 CPU 环境,以提高应用吞吐量。
  • CMS(Concurrent Mark Sweep)收集器:减少应用暂停时间,适用于对停顿时间敏感的应用。
  • G1(Garbage-First)收集器:目标是提供一种 GC 方式,它能在大型堆上提供高吞吐量并保证低延迟,自动调整停顿时间。

内存管理区域

Java 的内存空间被分为几个区域:

  • 堆(Heap):大多数对象都在堆上分配。这块内存是 GC 的主要关注区域。
  • 方法区(Method Area)/永久代(PermGen)/元空间(Metaspace):存储类信息、常量、静态变量等。
  • 栈(Stack):每个线程有自己的调用栈,存储局部变量和方法调用。
  • 程序计数器(PC Counters):每个线程的 JVM 指令执行计数器。
  • 本地方法栈(Native Method Stacks):用于支持本地方法的调用。

自动内存管理好处

  • 简化了编程:开发者无需担心内存分配和回收的细节。
  • 防止内存泄漏:理论上,由 GC 管理的内存模型不会发生内存泄漏,除非程序自身保留了不再需要的对象引用。
  • 提高性能:合适的 GC 配置可以提升应用性能。

注意事项

尽管 JVM 的垃圾回收机制大大简化了内存管理工作,但开发者仍需要:

  • 避免创建无用的对象引用,以免导致内存泄漏。
  • 合理配置 JVM 的内存参数,如堆大小、新生代和老年代的比例等。
  • 监控和调优 GC,以保证应用的性能和响应时间符合要求。

自动内存管理是 Java 技术栈中的一个重要优点,但正确和有效的使用它需要理解 GC 的工作原理和策略。通过调优 GC 参数和分析内存使用情况,可以提升 Java 应用的性能并确保系统的稳定运行。

6 Java 新特性

6.1 Java 8 新特性

Java 8 是 Java 语言一个重要的升级版本,它在 2014 年正式发布,引入了一系列影响深远的新特性和增强。以下是一些主要的 Java 8 新特性:

  1. Lambda 表达式

    • 引入了一种新的语法元素和操作符->来提供更简洁的编写匿名函数的能力。
    • 使得为函数式接口提供实现变得更加简单和灵活。
  2. 函数式接口

    • 可以配合 Lambda 表达式,引入了 @FunctionalInterface 注解来标识接口为函数式接口,即有且只有一个抽象方法的接口。
  3. Streams API

    • 为批量数据操作提供了一套新的 API 和抽象称为 Stream,允许你像查询数据一样查询对象集合。
    • 支持功能强大的内部迭代和透明的并行处理。
  4. 默认方法

    • 允许在接口中定义默认实现,有助于向旧版本的接口添加新方法而不破坏现有实现。
  5. 方法引用

    • 提供了一种方法引用的语法ClassName::methodName,允许更简短清晰地传递方法或构造函数。
  6. Optional 类

    • 提供一种新的类来避免空指针异常,可用于显式地要求某个变量可以为空。
  7. 新的时间日期 API

    • 引入了全新的时间日期 API,模仿 Joda-Time 库,简化了日期时间的处理。
  8. 接口中的静态方法

    • 允许在接口中定义静态方法。
  9. CompletableFuture

    • 提供了一个新的异步编程的工具,使得编写非阻塞的异步代码更加方便。
  10. Collectors 类

    • Streams API 中提供了 Collectors 类,其中定义了大量的方法来处理常见的聚合操作,如将 Stream 转换成集合或聚合元素。
  11. 新的集合库特性

    • 新增了很多便捷方法到 Collections 以及其他集合类中,如 ListMap 等。
  12. JVM 改进

    • 包括了对 PermGen Space 的移除和 Metaspace 的引入,以及 GC 的一些改进等。

这些新特性极大地丰富了 Java 语言,使得编写 Java 应用程序更加快捷、清晰,也进一步推动了 Java 向函数式编程范式的靠拢。

6.2 Java 9 新特性

Java 9 于2017年发布,带来了一系列重大新特性和改进。以下是 Java 9 中的一些关键特性:

1. 模块系统(Project Jigsaw)

Java 9 的最重要特性之一是引入了模块系统(也称为 Java Platform Module System,JPMS),这是对 Java 类加载架构的一次重大更新。模块系统旨在使 Java 应用更加可靠和可维护,同时提升大型应用的性能。

  • 模块化的 JDK:JDK 被分割成了一系列模块,可以让应用在运行时只包含必需的 JDK 部分,减小应用尺寸。

  • 模块:每个模块都包含一组相关的包、类和接口。

  • 模块声明:使用新的 module-info.java 文件来声明模块依赖、公开的 API 等。

2. JShell:交互式 Java REPL

JShell 是 Java 9 提供的交互式编程环境,允许开发者快速执行 Java 代码片段,而无需编写完整的程序。这是对快速原型开发和学习的一个重要工具。

3. 改进的 Javadoc

  • 搜索框:Javadoc 现在包含了一个搜索框,可以更容易地查找类和成员。

  • 支持 HTML5:Javadoc 现在可以输出为 HTML5 格式。

4. 新的集合工厂方法

Java 9 为 List、Set、Map 引入了便捷的静态工厂方法,使得创建不可变集合变得更加简单。

List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("a", "b", "c");
Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3);

5. 流 API 的增强

流 API 被进一步增强,添加了新的方法如 takeWhiledropWhileofNullable,以及对流的迭代器和分割迭代器的改进。

6. 改进的进程 API

Java 9 增强了对本地操作系统进程的控制功能,新增了 ProcessHandle API 来控制和监视系统进程。

7. Multi-Release JAR 文件

通过 Multi-Release JAR 文件,开发者可以创建包含针对多个 Java 版本优化过的类文件的 JAR。这样可以确保在不同版本的 Java 环境中都能运行相应的代码。

8. 统一的 JVM 日志系统

引入了新的 JVM 日志系统,统一了不同的 JVM 组件的日志输出。

9. 改进的 HTTP 客户端(Incubator)

引入了新的 HTTP/2 客户端 API,它支持 HTTP/2 协议和 WebSocket,并计划替换旧的 HttpURLConnection 类。原本作为一个孵化器模块被引入,后来在 Java 11 中成为正式特性。

这些仅是 Java 9 引入的众多新特性中的一部分。模块系统是最具革命性的变化,对于学习和使用 Java 已有业务来说是一个不小的挑战,但带来了更好的性能和应用模块化管理。Java 9 的许多新特性都在后续版本中得到了改进和继承。

6.3 Java 高版本新特性

随着 Java 不断地更新和发展,每个新版本都会带来一系列改进和新特性。这些特性旨在简化开发工作,提高性能,并且加强语言的功能。以下是从 Java 8 到最近几个版本(截至我知识库最后更新时间)新增的一些显著特性:

Java 8(March 2014)

  • Lambda 表达式:提供了一个清晰且简洁的方法来表示一次性匿名方法。
  • Stream API:新的抽象层,使得对集合的元素进行批量操作变得更加方便。
  • Date Time API:改进了日期和时间的处理。
  • 默认方法:接口中的默认方法使得无需实现类即可提供方法实现。
  • Nashorn JavaScript 引擎:允许在 JVM 上直接运行 JavaScript。

Java 9(September 2017)

  • 模块系统(Jigsaw 项目):引入了一种新的模块化方式来组织和封装代码。
  • JShell:交互式的 REPL(Read-Eval-Print-Loop)工具,用于快速测试和验证代码片段。
  • 改进的 Javadoc:添加了搜索功能和支持 HTML5 格式。

Java 10(March 2018)

  • 局部变量类型推断:引入了 var 关键字,可以在局部变量声明时省略显式类型。
  • 改进的垃圾收集器:加入了 G1 垃圾收集器以替换以往的并行处理器。

Java 11(September 2018)

  • HTTP 客户端 API:新的 HTTP 客户端库,支持 HTTP/2。
  • Lambda 参数的局部变量语法:允许在 lambda 表达式参数中使用 var
  • 运行单个 Java 文件的源码:直接运行单个源文件的 Java 程序,无需编译成 .class 文件。

Java 12(March 2019)

  • Switch 表达式(预览特性):改进了原有的 switch 语句,强化其作为表达式的使用。

Java 13(September 2019)

  • Text Blocks(预览特性):提供了多行字符串字面量的支持。
  • 更多的 switch 表达式改进(预览特性)。

Java 14(March 2020)

  • 记录类型(Record,预览特性):为声明纯数据载体类简化语法和编码方式。
  • Pattern Matching for instanceof(预览特性):提供了模式匹配的 instanceof 实现。

Java 15(September 2020)

  • Sealed Classes(预览特性):允许类或接口限制哪些其他类或接口可以扩展或实现它们。
  • 隐藏类:一种特殊的类,主要用于 Java 语言本身以及框架构建者的高级使用。

Java 16(March 2021)

  • Record 类成为正式特性:作为正式特性加入,简化了数据传输对象(DTO)的创建。
  • Pattern Matching for instanceof 成为正式特性:增强了代码的可读性和简洁性。

Java 17(September 2021)

  • 密封类(Sealed Classes)成为正式特性:对类的继承进行更细粒度的控制。
  • 新的 macOS 渲染管道:使用 Apple 的 Metal API 以改善 GUI 应用程序的性能。

注意

Oracle JDK 自 11 版本开始采取了新的商业授权。此外,Java 社区持续推进更快的发布周期,即每六个月发布一次。这意味着每个新版本会带来少量新特性,但更新更为频繁。

上述内容涵盖了高层次的特性概述。每个版本中还有大量的其他增强和性能改进。开发者应当根据具体需求考虑升级和利用新特性。在考虑升级时,还应该留意不同版本之间的兼容性问题。

7 Java Web 技术

7.1 Servlet 和 JSP

Servlet 和 JSP(JavaServer Pages)都是Java EE规范的一部分,用于开发基于Java的web应用程序。但是,Servlet 和 JSP 在它们的设计和使用上有一些关键区别。

Servlet

Servlet 是一种Java服务器端程序,它扩展了服务器的功能。一个 Servlet 是 Java类,它用于接收来自客户端的请求、处理它们,并生成对客户端的响应。

主要特点

  • 通常用于处理复杂的、需要编程逻辑的业务处理。
  • doGetdoPost 方法中获取请求数据、执行业务逻辑并生成响应。
  • 以Java代码中的 PrintWriter 输出响应,直接写入 HTTP 响应流。
  • 通常不适合编写大量的展示代码,因为会使 Servlet 代码变得混乱和不易维护。

JSP

JSP 是一种服务端的技术,允许将动态内容嵌入到静态的页面模板中。JSP 由两部分组成:静态数据(HTML 或 XML),以及嵌入其中的JSP动作和命令。

主要特点

  • 通常用于创建纯展示性的页面,并在其中嵌入少量逻辑来动态生成内容。
  • 可以使用自定义标签库(如 JSTL)来简化复杂逻辑的编写。
  • 由于它是页面导向的,因此更适合设计师和前端开发者编写。
  • 在运行时,JSP会被编译成Servlet,并按Servlet的方式进行处理。

Servlet 和 JSP 的交互

在实际开发中,Servlet 和 JSP 通常一起使用。Servlet 负责接收请求、执行业务逻辑。然后,将生成的数据(如模型、JavaBeans)传递给 JSP,由 JSP 生成响应的 HTML 视图。这种模式(模型-视图-控制器 MVC)使得逻辑处理和视图生成清晰分离,促进了应用程序各部分间的独立性和可重用性。

MVC 模式

在现代Web应用架构中,Servlet 通常扮演着控制器(Controller)的角色,而 JSP 作为视图(View)展示内容。模型(Model)则代表业务逻辑和数据的结构。这种分离的思想促进了应用程序更好的层次结构和更容易的维护性。

总结:
Servlet 更适合进行控制和逻辑处理;JSP 更适合表达和展示层次。两者结合,可以创建既强大又易于管理的 Web 应用程序。随着现代框架(如Spring MVC)的出现,这种分离思想被进一步强调,并提供了更多的自动化和约定。

7.2 Spring 框架

Spring框架是一个开源的Java平台,它被设计来简化Java应用的开发和提高开发者的生产力。Spring使用"约定优于配置"的原则,通过依赖注入(Dependency Injection)和面向切面编程(Aspect-Oriented Programming, AOP)等核心概念,实现了应用组件之间的松耦合。

Spring框架包含几个模块,每个模块都提供不同的功能:

核心容器(Core Container)

  • Spring Core:提供框架的基础部分,包括控制反转(IoC)和依赖注入(DI)功能。
  • Spring AOP:提供面向切面编程的实现,允许声明方法拦截器和切点来分离代码中的交叉关注点。
  • Spring Beans:提供Bean工厂和各种Bean的管理功能。
  • Spring Context:以BeanFactory为核心,提供了框架风格的方法如消息传递、国际化等。
  • Spring Expression Language:一种强大的表达式语言,用于在运行时查询和操作对象图。

数据访问/集成(Data Access/Integration)

  • Spring JDBC:简化了JDBC的使用,消除繁琐的JDBC编码和数据库资源的手动关闭。
  • Spring ORM:为流行的对象-关系映射API,如JPA、JDO和Hibernate,提供了集成层。
  • Spring OXM:提供了一种抽象层,允许通过OXM框架(如JAXB)进行对象/XML映射。
  • Spring JMS:提供了消息生产和消费功能。

Web模块(Web)

  • Spring Web MVC:一种富模块化的Web应用程序模型-视图-控制器(MVC)实现。
  • Spring WebFlux:为创建反应式Web应用程序提供支持。

AOP和设备支持(Aspects and Instrumentation)

  • Spring Aspects:对AspectJ的集成,提供了更丰富的AOP功能。
  • Spring Instrumentation:提供了类仪器支持和类加载器实现。

消息和集成(Messaging and Integration)

  • Spring Messaging:提供了消息支持的基础。
  • Spring Integration:提供了基于消息的应用程序集成的方式。

测试(Testing)

  • Spring Test:为Spring组件提供了测试支持,包括JUnit和TestNG。

Web容器(Spring Boot)

虽然并非Spring框架的一个模块,但 Spring Boot 为基于Spring的应用程序提供快速开发的方式,它可以轻松创建独立的、生产级的基于Spring的应用程序。

Spring框架的主要目标是使开发Java应用变得更加容易,其提供了丰富的开箱即用功能,允许开发者专注于业务逻辑而不必担心应用程序级别的服务。随着Spring生态系统的发展,现在包括多种项目,包括Spring Boot、Spring Data、Spring Security、Spring Cloud等,这些项目基于Spring框架构建,进一步简化了特定场景下的开发。

7.3 Hibernate 和 JPA

Hibernate 和 JPA 是 Java 生态系统中用于数据库操作的两个重要技术。

Hibernate

Hibernate 是一个流行的对象关系映射(Object-Relational Mapping, ORM)框架,它允许开发者透明地将 Java 对象映射到数据库表。这意味着开发者可以使用面向对象编程(OOP)的概念来操作数据库,而无需编写繁琐的 SQL 语句。Hibernate 提供了以下功能:

  • 数据持久化:将 Java 对象的状态保存到数据库中,并可以从数据库中检索。
  • 查询能力:支持 HQL(Hibernate Query Language)和原生 SQL 查询。
  • 缓存:提供了一级和二级缓存机制以提高查询效率。
  • 事务管理:支持声明性和编程性事务。
  • 和 JPA 的集成:Hibernate 也是 JPA 规范的一个实现。

Java Persistence API (JPA)

JPA 是 Java EE 环境中的一套 ORM 规范,它定义了对象/关系映射和数据持久化的标准 Java API。JPA 设计为一个规范层,允许开发者以不依赖特定厂商的方式操作关系型数据。以下是 JPA 提供的核心功能:

  • EntityManager:JPA 的主体部分,用于实体的操作和事务管理。
  • JPQL(Java Persistence Query Language):查询语言,用于基于实体的查询。
  • 实体映射:通过注解或 XML 文件定义 Java 对象和数据库表之间的映射关系。
  • ORM:提供了映射 Java 对象到数据库表的功能,包括 OOP 与数据库查询之间的桥梁。

Hibernate vs JPA

  • Hibernate 是 JPA 规范的具体实现。它在 JPA 的基础上提供了更多的特性,有些可能超出了 JPA 规范。这意味着 Hibernate 有些独特的功能,如级联扩展和自定义类型,这些可能在其他 JPA 实现中不存在。
  • JPA 是一层规范,旨在为 Java 开发者提供一致的 ORM 解决方案。使用 JPA,开发者可以在不同的 JPA 实现(如 Hibernate、EclipseLink、OpenJPA 等)之间进行切换,而无需改变数据持久化代码。这提供了很好的厂商无关性。

在实际使用中,开发人员可以通过 JPA 注解来使用 Hibernate 的具体实现,这样可以结合 JPA 的规范性和 Hibernate 提供的丰富特性。如果未来需要更换 ORM 框架,遵循 JPA 规范的代码更容易迁移。

选择直接使用 Hibernate 或者基于 JPA 的其他实现,通常取决于项目需求、团队偏好和未来可维护性的考量。

7.4 Web 服务和 RESTful API

Web 服务是一种可以通过网络进行交互、通信和数据交换的软件系统。它们通常使用一组标准化的协议和技术规范(如 HTTP、XML 和 SOAP)来实现不同系统之间的互操作性。RESTful API(Representational State Transfer)是一种特定类型的 Web 服务,它遵循 REST 架构风格。

Web 服务的关键概念:

  1. SOAP(Simple Object Access Protocol)

    • 一种基于 XML 的协议,用于在网络上交换结构化信息。
    • 它通过定义一组消息结构和消息处理方式,实现了在不同操作系统之间的通信功能。
  2. WSDL(Web Services Description Language)

    • 一种为 Web 服务描述其功能的标准描述语言。
    • WSDL 文档提供了服务的详细信息,如服务地址、提供的方法和参数等。
  3. UDDI(Universal Description, Discovery, and Integration)

    • 是一个由网络服务提供商所组成的全球性的 Web 服务信息注册中心合作。
    • 它允许服务提供方发布他们的 Web 服务,以便服务用户能发现和使用。

RESTful API 的关键概念:

  1. 无状态性(Statelessness)

    • 每个请求之间是独立的,请求中包含了所有必要的信息,从而服务端不需要保留之前的状态来理解请求。
  2. 资源(Resource)

    • 在 RESTful 设计中,一切都被视为资源,每个资源通过 URI 进行唯一标识。
    • 资源状态的表现形式可以是 JSON、XML 或其他任何可交互的格式。
  3. HTTP 动词

    • RESTful 使用标准的 HTTP 方法来处理资源,常用的有 GET、POST、PUT、DELETE。
  4. 资源之间的关系(HATEOAS - Hypermedia As The Engine Of Application State)

    • RESTful 设计原则之一是通过链接来表达资源之间的关系,使得客户端能够动态地发现其他可用的操作。

RESTful API 的优势:

  • 简单易懂:易于理解和实现,HTTP 动词提供了清晰、一致的方法来操作资源。
  • 可扩展性:它促进服务端的扩展性和互操作性。
  • 无状态性:提高了服务的可靠性,每个请求是自包含的。
  • 可缓存性:HTTP 中的缓存机制可以被利用,提高响应速度。
  • 多格式支持:客户端可以通过相同的资源地址,获取不同格式的资源表示,如 JSON、XML 等。

在 Java 中,RESTful API 的构建和消费可以使用 JAX-RS 规范和实现它的框架(如 Jersey、RESTEasy)来完成。Spring 框架中的 Spring MVC 也提供了一套简便的工具来构建 RESTful Web 服务。

7.5 安全性 (Spring Security)

Spring Security 是 Spring 生态系统中用于提供认证、授权和其他安全特性的强大和可定制的框架。它支持多种安全防护技术,包括 HTTP Basic authentication、表单登录认证、OAuth2、角色和权限控制(基于角色的访问控制,RBAC)、方法级安全性、LDAP、X.509 认证等。Spring Security 还能够与 Spring Boot 紧密集成,提供自动配置和简化的设置方式。

以下是 Spring Security 的几个主要特点:

认证(Authentication)

认证是核实用户身份的过程。Spring Security 提供了多种认证机制,例如通过用户名和密码、LDAP、基于表单的登录、OAuth2 技术(例如使用 Google 或 Facebook 进行登录)等。

授权(Authorization)

授权涉及的是确定已认证用户能访问的内容。在 Spring Security 中,你可以定义用户的权限或角色,并指定这些角色可以访问哪些资源。

CSRF 防护(Cross-Site Request Forgery)

Spring Security 提供了 CSRF 攻击防护的功能。CSRF 是一种攻击方式,正常的请求可能会被伪造。Spring Security 可以生成并验证请求中的 CSRF token。

XSS 防护(Cross-Site Scripting)

Spring Security 提供了输入验证和输出转义的功能,以防止因反射型和存储型 XSS 攻击导致的漏洞。

Session 管理

提供了会话固定保护、会话超时处理以及并发会话控制等功能。

HTTPS 和 TLS 支持

Spring Security 可以要求特定请求通过 HTTPS 来传输,并支持配置基于 HTTP 的强制安全传输。

使用 Spring Security 的一般步骤包括:

  1. 在项目的构建配置中添加 Spring Security 依赖:
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
}
  1. 创建 UserDetailsService 实现来提供用户信息,或者配置预定义的 UserDetailsService

  2. 编写 SecurityConfiguration 类,该类扩展了 WebSecurityConfigurerAdapter,在其中配置路由的安全规则、认证方式等。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()  // 公开访问的路径
                .anyRequest().authenticated()           // 其他地址的访问均需验证权限
            .and()
                .formLogin()
                .loginPage("/login").permitAll()       // 定义当需要用户登录时候,转到的登录页面
            .and()
                .logout().permitAll();                 // 登出行为任意访问
    }
}
  1. 若必要,实现其他安全相关的组件,如自定义 AccessDeniedHandlerAuthenticationEntryPoint 等。

Spring Security 提供了安全性专家们支持的建议和最佳实践,可以帮助你构建健壮和安全的应用程序。

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

golove666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值