Java基础

20 阅读1小时+

Java程序运行步骤

1,编写源代码

使用文本编辑器或集成开发环境(IDE)编写 Java 源代码。源代码以 .java 扩展名保存,通常包含类的定义、方法、变量和逻辑。

2,编译源代码

使用 Java 编译器(javac)编译源代码,将其转换为字节码文件。字节码文件以 .class 扩展名保存,包含了可由 JVM 执行的指令。

javac YourProgram.java

3,运行程序

使用 Java 虚拟机(JVM)运行编译后的字节码文件。在命令行中,使用 java 命令后跟类名(不含 .class 扩展名)来运行程序。

java YourProgram

4,JVM加载字节码

JVM 加载字节码文件到内存中,并执行其中的指令。类加载器负责加载类的定义。

5,JVM 执行 main 方法

如果在源代码中定义了 main 方法,JVM 会从该方法开始执行程序。main 方法是 Java 程序的入口点。

6,程序执行

JVM 按照 main 方法中的指令执行程序。您的程序可能包含输入、输出、逻辑判断、循环等等。

7,程序结束

程序执行完成后,或者 main 方法返回或抛出异常,程序将结束。JVM 会释放内存和资源。

概述

注释类型

1,单行注释:单行注释从 // 开始,直到行尾结束。它通常用于解释代码的细节、目的等。 2,多行注释:多行注释以 /* 开始,以 */ 结束。 3,文档注释:以 /** 开始,以 */ 结束,可以包含多行文本。文档注释是为了生成代码文档的一部分,可以使用工具生成 API 文档。

Java功能命令

生成文档

在 Java 中,您可以使用 javadoc 命令生成代码的文档。javadoc 是一个工具,可以从代码中的文档注释生成 API 文档,以便其他开发人员可以了解类、方法、字段的用途和使用方法。下面是使用 javadoc 命令生成文档的步骤:

  1. 编写文档注释: 在您的代码中,使用文档注释 /** ... */ 来描述类、方法、字段的功能、参数、返回值等信息。确保文档注释包含有关代码功能的详细描述。

    /**
     * This class represents a student.
     */
    public class Student {
        /**
         * The student's name.
         */
        private String name;
    
        /**
         * Constructs a new Student object.
         * @param name The name of the student.
         */
        public Student(String name) {
            this.name = name;
        }
    
        /**
         * Gets the name of the student.
         * @return The student's name.
         */
        public String getName() {
            return name;
        }
    }
    
  2. 使用 javadoc 命令生成文档: 打开命令行终端,导航到代码的根目录(包含要生成文档的源代码文件)。然后运行以下命令来生成文档:

    javadoc YourClass.java
    

    用实际的类名替换 YourClass。运行此命令后,javadoc 将分析代码中的文档注释并生成 HTML 格式的文档。

  3. 查看生成的文档: javadoc 命令将生成一个名为 index.html 的文件(以及其他文件和文件夹),您可以通过浏览器打开此文件查看生成的文档。文档将包含有关代码中类、方法、字段的详细说明。

命名规则

标识符

  • 标识符可以包含 Unicode 字符,因此可以使用非 ASCII 字符(但不推荐,因为可读性会受到影响)。

命名

  1. 类名:
    • 使用大驼峰命名法(CamelCase)。
    • 首字母大写,如 Person, Car, Student
  2. 变量名和方法名:
    • 使用小驼峰命名法(camelCase)。
    • 必须以字母、美元符号($)或下划线(_)开头,不能以数字开头。
    • '#','%'除外
    • 可以包含字母(大小写敏感)、数字和下划线(_)。
    • 首字母小写,如 firstName, calculateTotal, studentAge.
  3. 常量名:
    • 使用全大写字母,单词间使用下划线分隔。
    • MAX_VALUE, PI, DATABASE_URL.
  4. 方法名和变量名的意义清晰:
    • 使用有意义的名字,能够清楚地表达变量或方法的作用。
    • 避免使用单一字母作为变量名,除非用于短暂的循环控制等场景。
  5. 遵循驼峰命名法:
    • 在命名多个单词组成的标识符时,使用驼峰命名法,其中每个单词的首字母大写(类名除外)。
  6. 使用英语:
    • 尽量使用英语单词来命名,这有助于代码在全球范围内的共享和理解。
  7. 避免使用关键字:
    • 不要使用 Java 的关键字作为标识符。
  8. 注释:
    • 使用有意义的注释来解释代码的目的、思路或特殊情况。
  9. 可读性:
    • 使用空格和缩进来增强代码的可读性。

    • 保持代码的整洁,避免过多的冗余代码。

数据类型

在 Java 中,变量必须先声明再使用,声明时需要指定变量的数据类型。

基本数据类型

原始数据类型(Primitive Data Types)描述(字节)范围
整数类型
byte1-128 到 127
short2-32768 到 32767
int4-2147483648 到 2147483647
long8-9223372036854775808 到 9223372036854775807
浮点类型
float4IEEE 754 标准
double8IEEE 754 标准
字符型
char2'\u0000' 到 '\uffff'
布尔型
boolean用于表示 true 或 false 值true 或 false

引用数据类型

类(Class)定义了对象的模板,用于创建对象实例。
接口(Interface)定义了一组方法,类可以实现接口。
数组(Array)存储多个相同类型的元素。
枚举(Enumeration)一组命名的常量。
字符串(String)字符序列。
自定义引用类型开发人员可以定义自己的类和接口。
数组
  1. 声明和初始化数组: 在Java中,数组的声明和初始化可以一步完成,也可以分开进行。以下是两种常见的数组初始化方式:

    javaCopy code// 一步声明和初始化数组
    int[] numbers = {1, 2, 3, 4, 5};
    
    // 分开声明和初始化数组
    int[] numbers = new int[5]; // 创建一个长度为5的整数数组
    numbers[0] = 1;
    numbers[1] = 2;
    // 依此类推
    
  2. 访问数组元素: 数组的元素通过索引来访问,索引从0开始。例如,numbers[0] 表示数组中的第一个元素。

  3. 数组长度: 使用 array.length 可以获取数组的长度,它表示数组中的元素个数。

  4. 遍历数组: 使用循环(如 for 循环)可以方便地遍历数组中的所有元素。

    javaCopy codefor (int i = 0; i < numbers.length; i++) {
        System.out.println(numbers[i]);
    }
    
  5. 数组的动态性质: 数组的长度在创建后是固定的,不能直接调整。如果需要在运行时动态地增加或减少元素数量,可以使用集合(如 ArrayList)等数据结构。

  6. 数组的类型: 数组可以存储任何数据类型,包括基本数据类型和对象类型。

  7. 数组的内存分配: 数组在内存中是连续存储的,通过索引可以直接访问到数组的任何元素。在内存中,数组的分配是在堆内存中进行的。

数据类型转换

自动转换(小转大):

  • ​ 将取值范围小的类型自动提升为取值范围大的类型
  • ​ 如果一个操作数为double型,则整个表达式可提升为double型;
  • ​ 整型计算会自动转换成int而不是long

强制转换(大转小):将取值范围大的类型强制转换成取值范围小的类型

​ 数据类型 变量名 = (要转换成的数据类型)被转数据值;

单位换算
  • 位bit
  • 字节byte-B
  • KB
  • 1KB=1024B
  • 1B=8bit
控制台输入
Scanner sc = new Scanner(System.in);
String name = sc.next();

运算符

**++**在前:先加1,后运算 **++**在后:先运算,后加1

关系运算符
>,<,>=,<=,!=
逻辑运算符
运算符描述示例
&逻辑与运算符。如果两个操作数都为 true,结果为 true;否则为 false。true && false 结果为 false
|逻辑或运算符。如果两个操作数中至少有一个为 true,结果为 true;否则为 false。`truefalse结果为true`
!(非)逻辑非运算符。用于取反操作数的值。如果操作数为 true,则结果为 false;如果操作数为 false,则结果为 true。!true 结果为 false
^(异或)逻辑异或运算符。如果两个操作数的值不相等,结果为 true;如果两个操作数的值相等,结果为 false。true ^ false 结果为 true
&&短路与运算符。如果左边结果为false,直接返回结果,右边不参与运算。
||`运算符具有短路特性,这意味着如果第一个操作数为true,则不会对第二个操作数进行求值,因为已经可以确定整个表达式的结果为 true`。
boolean condition1 = true;
boolean condition2 = false;

boolean result1 = condition1 || condition2; // 结果为 true
boolean result2 = condition2 || condition1; // 结果为 true
boolean result3 = condition2 || condition2; // 结果为 false

boolean result1 = condition1 && condition2; // 结果为 false
boolean result2 = condition2 && condition1; // 结果为 false
boolean result3 = condition1 && condition1; // 结果为 true
  1. 条件运算符(三元运算符):? :
  2. 赋值运算符:=, +=, -=, *=, /=, %=, &=, ^=, |=, <<=, >>=, >>>=

流程控制语句

1,条件语句

if 语句: if(条件){};

​ 用于执行条件判断,根据条件的真假来选择执行不同的代码块。

switch 语句: 用于根据表达式的值从多个选项中选择一个执行分支。

switch (expression) {
    case value1:{
         // 代码块
        break;
    } default:{
        ....
    }
  }

2,循环语句

for 循环: 用于重复执行一段代码,通常在已知循环次数的情况下使用。

for (initialization; condition; increment) {  // 代码块}

while 循环: 在条件为真的情况下重复执行一段代码,先判断条件后执行代码。

while (condition) { // 代码块}

do-while 循环: 类似于 while 循环,但是先执行一次代码块,再判断条件是否为真。

do {// 代码块} while (condition);

3,跳转语句

  • break 语句: 用于跳出循环或 switch 语句。
break;
  • continue 语句: 用于结束当前循环迭代,继续下一次迭代。
continue;
  • return 语句: 用于从方法中返回值,同时结束方法的执行。
return value;

方法

**作用:**方法(Method)是一段可重用的代码块,用于执行特定的任务或操作。方法允许将代码组织成逻辑单元,并通过名称来调用。在 Java 中,几乎所有的代码都是在方法中执行的。

**优势:**可以帮助组织和管理代码,提高代码的可读性、提高开发效率、可维护性和重用性。

  1. 方法定义

    方法名(Method Name): 方法的唯一标识符,用于在代码中调用方法。

    参数列表(Parameter List): 方法可以接受输入参数,参数列表定义了方法接收的参数类型和名称。

    返回类型(Return Type): 方法可以返回一个值,返回类型指定了方法返回的数据类型。如果方法不返回任何值,可以使用 void 关键字表示。

    方法体(Method Body): 方法体包含了实际执行的代码块,用于完成特定的任务。

    // 定义一个方法,接受两个整数参数并返回它们的和
        public static int add(int a, int b) {
            int sum = a + b;
            return sum; // 返回计算结果
        }
    
  2. 方法重载 是指在同一个类中定义多个同名的方法,但这些方法的参数列表不同。重载的方法具有相同的方法名,但不同的参数类型、参数个数或参数顺序。在调用时,编译器会根据方法的参数列表选择合适的重载方法进行调用。

  3. 参数传递

    值传递(Pass-by-Value): 值传递是指将方法调用时的实际参数的值复制一份,然后将复制的值传递给方法内部。在方法内部对参数的操作不会影响原始数据。Java 中的基本数据类型(如整数、浮点数等)是通过值传递传递的。

    引用传递(Pass-by-Reference): 引用传递是指将方法调用时的实际参数的引用(内存地址)传递给方法内部。这意味着在方法内部对参数的操作会影响原始数据。Java 中的对象和数组是通过引用传递传递的。

    String :String 类型的参数被final修饰为不可变参数。当声明字符串时会先检查字符串常量池中有没有相等的值,如果有直接引用,否则直接在栈中创建新的字符串对象(对值的每一次更改都会创建新对象),将值添加到字符串常量池中。

public class ReferencePassingExample {
    // 方法,接受一个字符串参数并在末尾添加内容
    public static void appendString(String s) {
        s += " World";
    }

    public static void main(String[] args) {
        String str = "Hello";

        System.out.println("原始字符串:" + str);

        // 调用方法修改字符串
        appendString(str);

        System.out.println("修改后的字符串:" + str); // 这里仍然输出 "Hello"
    }
}
在这个示例中,我们定义了一个名为 appendString 的方法,它接受一个字符串参数并在字符串末尾添加 " World"。然而,在 main 方法中,虽然我们调用了 appendString 方法,但是原始字符串 str 并没有被修改,因为字符串在方法内部被复制了一份引用。

要实现真正的引用传递,可以使用可变的引用类型,比如数组。这样,在方法中修改数组的元素会影响到原始数组。

类和对象

类(Class): 类是一种模板或蓝图,用于定义对象的属性(成员变量)和行为(方法)。它是一种抽象的概念,用于描述具有相似属性和行为的一组对象(对象的集合)。

对象(Object): 对象是类的实例化,是一个具体的实体,具有类定义的属性和行为。对象是实际存在的。

Static

静态变量(Static Variables): 在类中使用 static 关键字声明的变量称为静态变量,也叫类变量。静态变量属于类,而不是类的实例(对象),并且在所有类的实例之间共享相同的值。可以通过类名来访问静态变量,而不需要创建类的实例。

静态方法(Static Methods): 在类中使用 static 关键字声明的方法称为静态方法。静态方法属于类,而不属于类的实例。静态方法可以直接通过类名调用,而不需要通过对象实例调用。静态方法通常用于实现工具方法,不需要访问对象的状态。

static 关键字的主要作用是创建与类本身关联,而不是与类的实例关联的元素。这些元素可以在类加载时就被初始化,而不需要创建对象实例。静态成员在内存中只有一份副本,可以在整个类中共享。 static关键字修饰的变量或方法,只能访问或修改static所修饰的变量或方法。非静态方法既可以访问静态方法和变量,也可以访问非静态变量和方法

成员变量与局部变量

特点成员变量局部变量
定义位置在类的内部、方法的外部定义在方法内部、代码块内部、循环内部等局部范围内定义
作用域整个类中都可见和访问定义所在的方法或代码块中可见和访问
生命周期随着对象的创建而存在,对象销毁时被销毁随着方法或代码块的执行而存在,执行结束后被销毁
初始化成员变量可以有默认初始值,也可以在构造方法中初始化局部变量没有默认初始值,需要显式初始化才能使用
访问权限可以使用访问修饰符控制访问权限不可以使用访问修饰符控制访问权限
存储位置存储在堆内存中的对象中存储在栈内存中,方法执行结束后立即释放
静态或实例可以是静态的(使用 static 关键字)或实例的永远是实例的(不能用 static 关键字修饰)
使用场景用于保存对象的属性和状态用于保存临时数据和控制流程中的值

构造方法

构造方法(Constructor)是一种特殊类型的方法,用于在创建对象时初始化对象的状态。构造方法的主要目的是为了确保对象在创建时具有合适的初始值,并执行必要的设置操作。

  1. 命名与特点: 构造方法的名称与类名相同,但没有返回类型,甚至不需要 void 关键字。它在对象创建时被自动调用。
  2. 默认构造方法: 如果在类中没有显式定义构造方法,Java 会为类提供一个默认的无参构造方法。默认构造方法不执行任何特定的初始化操作。如果定义了构造方法,以此为默认方法,创建对象时直接调用。
  3. 构造方法重载: 通过在同一个类中定义不同参数列表的构造方法,可以实现构造方法的重载。这使得使用不同的参数创建对象变得更灵活。
  4. 构造方法调用: 构造方法通常通过 new 关键字来调用,创建一个新的对象实例。在调用构造方法时,分配内存空间以存储对象,并执行构造方法的代码。
  5. 构造方法链: 构造方法可以通过调用其他构造方法来实现代码复用。这种方式被称为构造方法链。
  6. 构造方法的作用: 构造方法用于初始化对象的状态,为成员变量赋初值,执行其他必要的设置操作。在构造方法中可以实现对象的初始化逻辑,以确保对象在创建时处于一种合理的状态。

使用 this 关键字: 可以使用 this 关键字来引用当前的对象。这在避免参数和成员变量名称冲突时很有用。

JavaBean

  1. 公共构造方法: JavaBean 必须有一个无参构造方法和全参构造方法,以便其他组件可以使用 new 关键字创建它的实例。
  2. 私有成员变量: JavaBean 通常包含私有的成员变量,这些成员变量用于存储对象的状态数据。
  3. 公共的 getter 和 setter 方法: JavaBean 通过公共的 getter 和 setter 方法来访问和修改成员变量的值。这些方法通常遵循命名约定,如 getPropertyName()setPropertyName()
  4. 可序列化: JavaBean 可以实现 Serializable 接口,以支持对象的序列化和反序列化,使得对象可以在网络传输和持久化存储中使用。

super

  1. 调用父类构造方法: 在子类的构造方法中,可以使用 super() 来调用父类的构造方法。这样可以确保父类的初始化过程得以执行。
  2. 调用父类成员变量和成员方法: 在子类中,可以使用 super 关键字来引用父类的成员变量和成员方法。如果子类中存在与父类同名的成员变量或方法,可以使用 super 来明确指定调用父类的成员。
  3. 访问父类中被隐藏的成员: 当子类中定义了与父类同名的成员时,父类的成员会被隐藏。使用 super 关键字可以在子类中访问被隐藏的父类成员。
  4. 调用父类构造方法时传递参数: 在子类的构造方法中,可以使用 super(参数列表) 来调用父类的特定参数构造方法。

final

  1. final 类: 当一个类被声明为 final 类时,意味着该类不能被继承。换句话说,不可以创建该类的子类。通常用于防止类的进一步扩展,或者表示该类已经完整实现且不应被修改。
  2. final 方法: 当一个方法被声明为 final 方法时,意味着该方法不能在子类中被重写。子类无法改变父类中的 final 方法的实现。通常用于确保父类的某些方法在继承层次中不会被修改。
  3. final 变量(常量): 当一个基本数据变量被声明为 final 变量时,意味着该变量只能被赋值一次,且不能再次改变其值。通常用于声明常量,一旦赋值后就不可修改。final 变量的命名通常使用大写字母和下划线。

三大特征

封装

封装是面向对象编程中的一个特征,它将数据和操作数据的方法封装在一个单元中,并限制外部对数据的直接访问。通过封装,对象的内部实现细节得以隐藏,只暴露必要的公共接口供外部使用,提高了代码的安全性、可维护性和可扩展性。封装可以理解为将对象的状态和行为包装在一起,形成一个独立的、具有特定功能的实体。

继承
  1. 代码重用: 继承允许子类继承父类的属性和方法,从而避免了重复编写相同的代码。这样可以提高代码的重用性,减少冗余,降低开发成本。
  2. 代码扩展: 子类可以在父类的基础上扩展功能,添加新的属性和方法。这样在不影响原有功能的情况下,可以根据需求灵活地增加新特性。
  3. 抽象和模块化: 继承可以将具体的类和通用的类分离,通过抽象出通用属性和方法,达到模块化的目的。

特点

  1. 默认构造方法的继承: 如果父类有默认(无参)构造方法,并且子类没有显式定义构造方法,那么子类会自动继承父类的默认构造方法。这是因为在子类的构造方法中,会默认调用父类的无参构造方法。
  2. 显示调用父类构造方法: 在子类的构造方法中,可以使用 super 关键字来显式调用父类的构造方法。这通常发生在子类需要执行一些额外的初始化操作或者需要传递参数给父类构造方法时。
  3. 构造方法不继承: 子类不会继承父类的构造方法本身。子类只是继承了父类的成员变量和方法,但构造方法不会被继承。尽管如此,子类仍然可以通过调用 super 来使用父类的构造方法。
  4. 默认构造方法和显示调用: 如果子类的构造方法中没有显式调用父类的构造方法,且父类没有无参构造方法,则会导致编译错误。因为 Java 在子类的构造方法中会默认添加一个隐式的 super() 调用,如果父类没有无参构造方法,编译器会报错。

子类中所有的构造器都会默认调用父类中的无参构造器,因为每一个子类构造器内的第一行都有一条隐式的super()

尽管继承带来了许多好处,但在使用时也应谨慎考虑。过度的继承可能导致类的复杂性增加、耦合度加强,从而降低代码的可维 护性。正确的继承设计可以在合适的情况下发挥其优势,并避免其缺点。

多态
  1. 要有继承关系的实现
  2. 方法重写(覆盖): 子类可以重写(覆盖)父类的方法,从而实现不同的具体行为。
  3. 父类引用指向子类对象: 父类的引用变量可以指向其子类的对象,通过这个引用变量,可以调用子类特有的方法,实现不同的行为。

抽象类和接口

抽象类

抽象类是 Java 中的一个特殊类,它用于作为其他类的基类,但本身不能被实例化。抽象类允许定义抽象方法和具体方法,这些方法可以被子类继承和实现。抽象类的主要特点是:

  1. 不能被实例化: 抽象类不能直接创建对象,因为它本身是不完整的,包含了未被实现的抽象方法。需要通过子类来实现抽象方法,然后才能创建子类的对象。
  2. 可以包含抽象方法: 抽象类可以包含抽象方法,这些方法没有具体的实现,只有方法的声明。子类继承抽象类后,必须实现抽象方法,否则子类也必须声明为抽象类。
  3. 可以包含具体方法: 抽象类也可以包含具体的方法,这些方法有实际的实现。子类继承抽象类后,可以直接继承和使用这些具体方法。
  4. 用于定义通用行为: 抽象类通常用于定义一些通用的属性和方法,用于被其子类继承和扩展。它们提供了一种将公共代码和行为封装在一起的方式。
接口(Interface)

接口不能实例化,可以通过它的实现类来实现,接口中的方法默认是抽象方法,接口中的变量默认常量static(静态的),在JDK8之后,在接口中定义default方法可以有方法体。静态方法也可以有方法体。

  1. 抽象方法集合: 接口是一种抽象的数据类型,它定义了一组抽象方法的集合,但没有具体的实现。实现接口的类必须提供这些抽象方法的具体实现,使用implements实现接口。
  2. 多继承: Java 中的类只支持单继承,但一个类可以实现多个接口。这使得类可以在保持继承关系的同时,实现多个不同的行为契约。接口与接口之间是继承关系,在接口可以实现多继承。
  3. 解耦合: 接口可以帮助实现代码的解耦合,通过将接口和实现分离,可以使代码更具扩展性和可维护性。不同的类可以实现相同的接口,从而保证一致的行为。
  4. 规范和约束: 接口定义了一种规范和契约,实现了接口的类必须按照接口定义的方法签名和语义来实现,确保了代码的一致性和规范性。
  5. 实现多态性: 接口实现了多态性的一种形式。通过接口的引用,可以在运行时指向不同的实现类对象,实现不同的行为。

异常

在Java中,处理异常的主要方式是使用try-catch块和抛出语句。这两种方式用于捕获和处理程序中可能发生的异常,从而使程序在异常情况下能够更加健壮。

使用try-catch块: 在需要捕获异常的代码块中使用try-catch块,try块中包含可能引发异常的代码,catch块中捕获并处理。

使用 throws 声明: 如果方法可能会引发异常,但方法本身不处理异常,可以在方法签名中使用 throws 关键字声明可能的异常。这样调用该方法的代码就需要处理这些异常。

**使用throw抛出:**使用 throw 关键字,后跟异常类(new Exception),将异常抛出。这会立即终止当前方法的执行,并将异常传递给调用栈中的上层调用者。如果上层调用者也没有处理这个异常,那么异常会继续向上传递,直到某个地方捕获并处理该异常,或者程序终止。

异常分为两种主要类型:受检异常(Checked Exception)和非受检异常(Unchecked Exception)

  1. 受检异常(Checked Exception):也被称为编译时异常,这些异常在编译代码时就需要进行处理,否则编译不通过。受检异常通常表示程序中可能发生的外部情况,比如I/O错误、文件不存在等。开发者必须在代码中使用try-catch块或者在方法签名中使用throws关键字声明受检异常的抛出。
  2. 非受检异常(Unchecked Exception):也称为运行时异常,这些异常在编译时不需要强制处理。非受检异常通常表示程序内部错误或逻辑问题,如空指针异常( NullPointerException )、数组越界异常(ArrayIndexOutOfBoundsException)等。这些异常通常是由程序员的错误引起的,应该被修复。

finally

用于定义一个代码块,该代码块中的代码无论是否发生异常,都会被执行。

  1. finally块中的代码总会被执行,无论是否发生异常,包括正常结束和异常结束。
  2. finally块可以单独存在,也可以与try尝试try-catch`块一起使用。
  3. 如果finally块有return语句,它会覆盖之前在trycatchreturn语句,但不会改变方法的异常。
  4. finally块通常用于释放资源、关闭连接、清理等。

JavaAPI

==操作符:

  • 基本数据类型(Primitive Types):对于基本数据类型(如整数、浮点数等),== 比较的是它们的值是否相等。如果两个基本数据类型的值相等,则返回 true,否则返回 false
  • 引用类型(Reference Types):对于引用类型(如对象、数组等),== 比较的是它们在内存中的引用是否相等,即它们是否指向同一块内存地址。如果两个引用指向同一块内存地址,则返回 true,否则返回 false

String

  1. 不可变性String 对象是不可变的,意味着一旦创建就无法修改其内容。每次对字符串进行操作都会创建一个新的字符串对象。这可能会导致内存占用和性能问题。在需要频繁修改字符串的情况下,最好使用 StringBuilderStringBuffer
  2. 字符串连接:使用 + 运算符连接字符串可能会导致性能下降,特别是在循环中进行字符串拼接。这是因为每次拼接都会创建一个新的字符串对象。在循环中拼接字符串时,应该使用 StringBuilderStringBuffer 来提高性能。
  3. 字符串池:Java中存在一个字符串池(String Pool),用于存储字符串常量,以便在重复使用时减少内存消耗。使用字面量(如 "hello")创建字符串时,JVM会检查字符串池中是否已经存在相同内容的字符串。但使用 new String() 构造函数创建的字符串不会在池中,而是在堆内存中创建。
  4. equals() 方法:用于比较字符串内容是否相等。在比较字符串时,尽量使用 equals() 方法而不是 == 操作符,因为 equals() 比较的是内容,而 == 比较的是引用。
  5. 字符串格式化:使用 String.format() 方法可以根据格式模板将多个值格式化为字符串。这在输出和日志记录时非常有用。
  6. null:避免将字符串变量赋值为 null,以防止空指针异常。如果需要表示空字符串,最好使用空字符串 ""
  7. 字符串不可变性的优势:虽然字符串不可变性可能导致一些性能问题,但它也带来了线程安全性和缓存优化等方面的好处。在大多数情况下,选择正确的字符串处理方式很重要。
方法描述
length()返回字符串的长度。
charAt(int index)返回索引位置上的字符。
concat(String str)将指定的字符串连接到该字符串的末尾。
equalsIgnoreCase(String another)忽略大小写比较字符串内容是否相等。
indexOf(String str)返回子字符串在父字符串中的索引位置。
lastIndexOf(String str)返回子字符串在父字符串中的最后索引位置。
startsWith(String prefix)检查字符串是否以指定前缀开头。
endsWith(String suffix)检查字符串是否以指定后缀结尾。
trim()去除字符串前后的空格。
replace(char oldChar, char newChar)替换字符串中的字符。
split(String regex)将字符串按照指定的正则表达式分割成字符串数组。
contains(CharSequence seq)检查字符串是否包含指定的字符序列。

StringBuilder

常用方法:

  1. append(String str):在字符串末尾追加指定的字符串。
  2. insert(int offset, String str):在指定位置插入指定的字符串。
  3. delete(int start, int end):删除从 startend - 1 之间的字符。
  4. replace(int start, int end, String str):用指定的字符串替换从 startend - 1 之间的字符。
  5. toString():将 StringBuilder 转换为普通的字符串对象。

StringBuffer方法同StringBuilder

注意事项:

  1. StringBuilder 是可变的,但不是线程安全的。如果在多线程环境中使用,应该考虑使用 StringBuffer,它是线程安全的版本。
  2. 避免频繁地创建新的 StringBuilder 实例,最好在一个实例上执行多个操作,以减少内存开销。
  3. 虽然 StringBuilder 的效率通常很高,但在处理大量字符串拼接时,仍然可能导致性能问题。在这种情况下,可以考虑使用 java.util.StringJoiner(Java 8及更高版本)或StringBuffer

StringBuilderStringBuffer 两者之间的区别

关于这两者的区别,最主要的是线程安全性和性能方面。StringBuilder 适用于单线程环境下的字符串拼接,由于它不是线程安全的,所以在多线程环境下使用时需要谨慎。StringBuffer 则是线程安全的,适合在多线程环境下进行字符串操作,但它在性能方面通常稍慢,因为它会进行同步操作以保证线程安全。

特性StringBuilderStringBuffer
线程安全性非线程安全线程安全
性能通常比 StringBuffer通常比 StringBuilder
同步支持不支持同步支持同步
方法返回类型返回 StringBuilder返回 StringBuffer
可变性可变可变
用途在单线程环境下使用在多线程环境下使用

类型转换

从包装类型到String方法
使用 toString() 方法包装类型对象.toString()
使用字符串拼接"" + 包装类型对象

日期类

从String到包装类型方法
使用包装类的静态方法包装类型.valueOf(String)
包装类型.parseXxx(String)(如Integer.parseInt(String)
Date()
方法名说明
public Date()分配一个 Date对象,并初始化,以便它代表它被分配的时间,精确到毫秒
public Date(long date)分配一个 Date对象,并将其初始化为表示从标准基准时间起指定的毫秒数
Date类常用方法
方法名说明
public long getTime()获取的是日期对象从1970年1月1日 00:00:00到现在的毫秒值
public void setTime(long time)设置时间,给的是毫秒值
SimpleDateFormat常用方法
方法描述
format(Date date)将给定的 Date 对象格式化为字符串,按照 SimpleDateFormat 对象指定的日期格式。
parse(String source)将给定的字符串解析为 Date 对象,根据 SimpleDateFormat 对象指定的日期格式进行解析。
setTimeZone(TimeZone zone)设置时区,以影响日期格式化和解析的结果。
applyPattern(String pattern)设置日期格式模式。这将更改日期格式化和解析的模式。
toPattern()获取当前日期格式的模式字符串。
toLocalizedPattern()获取当前日期格式的本地化模式字符串。

集合

泛型

在Java中,泛型最常见的应用是在集合类中,如ArrayList、LinkedList、HashMap等,它们可以容纳各种不同类型的对象,并在编译时提供类型检查

泛型的语法通常使用尖括号 < > 中的类型参数来表示。

<? super E>:表示通配符泛型,其中 ? 表示未知类型,super 表示可以接受 E 类型或 E 的超类型的元素。它用于表示可以接受某种特定类型或该类型的父类的集合。通常用于写入(添加)元素。

<? extends E>:同样表示通配符泛型,其中 ? 表示未知类型,extends 表示可以接受 E 类型或 E 的子类型的元素。它用于表示可以接受某种特定类型或该类型的子类的集合。通常用于读取(获取)元素。

List

List排序方式

List集合中的元素是按照它们被插入的顺序排列的,这意味着元素的顺序与它们被添加到列表的顺序一致,在List中,元素的顺序是明确的,它们按照被添加的顺序存储和访问。

List接口的实现类(如ArrayList、LinkedList)都遵循这个顺序规则。因此,当迭代List集合时,元素的顺序将保持不变,除非你明确地执行了排序操作。

ArrayList与LinkedList

ArrayList和LinkedList是两种常见的Java集合(List)实现,它们之间的主要区别在于底层数据结构、随机访问、插入/删除操作和内存占用等方面。

  1. 底层数据结构
    • ArrayList使用动态数组(数组)作为底层数据结构,元素在内存中是连续存储的。这意味着ArrayList对随机访问的支持非常好,可以通过索引快速访问元素
    • LinkedList使用双向链表作为底层数据结构,元素在内存中是分散存储的,每个元素都包含对前一个和后一个元素的引用。这导致LinkedList的随机访问效率相对较差,因为需要遍历链表来查找元素。
  2. 随机访问时间复杂度
    • ArrayList的随机访问时间复杂度是O(1),因为它可以通过索引直接访问数组中的元素。
    • LinkedList的随机访问时间复杂度是O(n),需要从链表的头部或尾部开始遍历,最坏情况下是O(n/2)。
  3. 插入/删除时间复杂度
    • ArrayList在中间位置插入/删除元素时,需要移动元素,时间复杂度是O(n)(平均情况下,最坏情况下为O(n/2))。
    • LinkedList在列表中任意位置插入/删除元素时,不需要移动元素,时间复杂度是O(1)。但在列表两端进行插入/删除时,仍然是O(1)。
  4. 内存占用
    • 通常情况下,ArrayList比LinkedList更节省内存,因为它不需要为每个元素存储额外的引用。
    • LinkedList通常比ArrayList占用更多内存,因为它需要为每个元素存储前一个和后一个元素的引用。
  5. 迭代器性能
    • 在迭代元素时,ArrayList的性能通常优于LinkedList,因为它可以通过索引直接访问元素。
    • LinkedList的迭代性能相对较差,因为需要遍历链表。
  6. 添加/删除元素
    • ArrayList适用于在列表末尾添加/删除元素的情况。
    • LinkedList适用于在列表任意位置添加/删除元素的情况,因为插入/删除操作的时间复杂度是O(1)。

根据具体的应用场景和操作需求,可以选择ArrayList或LinkedList。如果需要高效的随机访问和较少的插入/删除操作,ArrayList通常更适合。如果需要高效的插入/删除操作,尤其是在列表中部进行操作,或者内存占用相对较少的情况下,LinkedList可能更适合。

Set

Set集合的特点

  • 不可以存储重复元素。
  • 没有索引,不能使用普通for循环遍历。
HashSet
  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化;
  • HashSet不是同步的,如果多个线程同时访问或修改一个 HashSet,则必须通过代码来保证其同步;
  • 集合元素值可以是 null;
TreeSet

可以将元素按照规则进行排序。

自然排序,JavaBean类实现Comparable接口排序,重写compareTo方法,在方法中自定义排序规则。

比较器排序,编写比较器类,实现Comparator接口,重写compare方法,在方法中自定义排序规则。

Collections(Class)

copy(List<? super T> dest, List<? extends T> src) 方法:

  • dest:目标列表,要将元素复制到的列表。
  • src:源列表,要从中复制元素的列表。

作用:将源列表 src 中的元素复制到目标列表 dest 中。目标列表的大小必须大于或等于源列表的大小。

addAll(Collection<? super T> c, T... elements) 方法:

  • c:要将元素添加到的集合。
  • elements:要添加到集合 c 的元素(可变参数列表)。

作用:将指定的元素添加到集合 c 中。

sort(List<T> list) 方法:

  • list:要排序的列表。

作用:对列表中的元素进行升序排序,要求列表中的元素必须实现 Comparable 接口或提供自定义的比较器。

sort(*List*<T> list, *Comparator*<? *super* T> c)方法:可以根据指定比较器引发的顺序对指定列表进行排序。

Map

  1. 键值对Map 存储的数据是键值对,每个键都唯一,并且与一个值相关联。这允许你使用键来检索、插入和删除与之关联的值。
  2. 无序性Map 不保证元素的存储顺序,不像List中的元素是按照索引顺序存储的。不同的Map 实现可以以不同的方式存储元素。
  3. 键的唯一性:每个键在Map中必须是唯一的,如果尝试使用已经存在的键插入新的值,那么旧的值将被替换。
  4. 值的重复性Map 允许不同的键关联到相同的值,即值可以重复。
  5. 泛型支持Map 是泛型的,这意味着你可以指定键和值的数据类型。例如,你可以创建一个Map<String, Integer>来将字符串键映射到整数值。
HashMap
  • 作用:HashMap 是一个散列表实现,用于存储键值对。它提供了快速的查找性能,对于大多数情况下,它是一个高效的选择。
  • 特点:
    • 键是唯一的,不允许重复键。
    • 不保证元素的顺序,即无序。
    • 允许 null 键和 null 值。
    • 时间复杂度:平均情况下,查找、插入和删除操作的时间复杂度是 O(1)。
TreeMap
  • 作用:TreeMap 是一个基于红黑树实现的有序映射。它将键值对按照键的自然顺序或者根据自定义比较器进行排序。

  • 特点:

    • 键是唯一的,不允许重复键。
    • 键是有序的,按照键的自然顺序或者自定义比较器的顺序排列。
    • 不允许 null 键,但允许 null 值。
    • 时间复杂度:插入、删除和查找操作的平均时间复杂度为 O(log n)。

    根据具体的需求选择使用哪种 Map 类型,如果需要按键排序或者自定义排序方式,TreeMap 是一个不错的选择,否则,HashMap 通常具有更好的性能。

I/O

File

File 类是 Java 中用于处理文件和目录的类,它提供了对文件系统中的文件和目录进行创建、删除、查找、重命名、遍历等操作的方法。File 类是 Java I/O API 的一部分,它使得你可以在文件系统中操作文件和目录,是文件操作的基础。

作用

  1. 文件和目录操作File 类允许你创建文件和目录,删除文件和目录,重命名文件和目录,以及查找文件和目录。
  2. 路径操作:可以使用 File 类来构建、解析和操作文件路径。
  3. 文件属性查询:你可以查询文件和目录的属性,如文件大小、最后修改时间、是否可读等。
  4. 目录遍历File 类提供了方法来遍历目录中的文件和子目录。
方法描述
File(String pathname)使用给定的路径名创建一个新的 File 对象。
File(String parent, String child)使用给定的父路径和子路径创建一个新的 File 对象。
boolean exists()判断文件或目录是否存在。
boolean isFile()判断是否是一个文件。
boolean isDirectory()判断是否是一个目录。
String getName()获取文件或目录的名称。
String getPath()获取文件或目录的路径。
boolean createNewFile()创建一个新文件(如果不存在)。
boolean mkdir()创建一个新目录(如果不存在)。
boolean delete()删除文件或目录。
File[] listFiles()返回目录中的所有文件和子目录的 File 对象数组。

递归

递归(Recursion)是一种在计算机编程和数学中常见的概念,它指的是一个函数或过程在执行的过程中调用自己的行为。递归是通过将一个问题分解为一个或多个与原问题相似但规模较小的子问题来解决问题的方法。在递归中,问题的解决方案依赖于对较小子问题的递归调用。

递归通常包括两个重要的部分:

  1. 基本情况(Base Case):这是递归算法的终止条件。在递归函数中,基本情况是一种情况,它不再进行递归调用,而是直接返回结果。基本情况的存在是为了防止递归无限循环,确保递归最终会结束。
  2. 递归步骤:这是递归算法中的重复步骤。在递归步骤中,问题被分解为一个或多个规模较小的子问题,然后递归调用相同的函数来解决这些子问题。这些子问题的解决结果最终合并以解决原始问题。

字节流(Byte Stream)

InputStream 常用方法:

  1. int read(): 从输入流中读取一个字节的数据并返回,如果到达文件末尾则返回 -1。
  2. int read(byte[] b): 从输入流中读取多个字节的数据到字节数组 b 中,并返回实际读取的字节数。如果到达文件末尾则返回 -1。
  3. void close(): 关闭输入流。

OutputStream 常用方法:

  1. void write(int b): 将一个字节写入输出流。
  2. void write(byte[] b): 将字节数组 b 中的数据写入输出流。
  3. void close(): 关闭输出流。

字符串转换为字节使用getByte()方法

字节缓冲流(BufferedInputStream 和 BufferedOutputStream):

字节缓冲流用于读取和写入字节数据。

常用构造函数:

  • BufferedInputStream(InputStream in):创建字节输入缓冲流。
  • BufferedOutputStream(OutputStream out):创建字节输出缓冲流。

常用方法:

  • read(byte[] buffer, int offset, int length):从输入流中读取字节到缓冲区。
  • write(byte[] buffer, int offset, int length):将字节从缓冲区写入输出流。
  • close():关闭流。
  • flush():刷新缓冲区,将数据写入底层流。

字符流(FileReader 和 FileWriter)

字符流用于读取和写入字符数据,通常用于处理文本文件。

常用构造函数:

  • FileReader(String fileName):创建字符输入流。
  • FileWriter(String fileName):创建字符输出流。

常用方法:

  • read(char[] buffer, int offset, int length):从输入流中读取字符到缓冲区。
  • write(char[] buffer, int offset, int length):将字符从缓冲区写入输出流。
  • close():关闭流。

字符缓冲流(BufferedReader 和 BufferedWriter)

字符缓冲流是字符流的包装,提供了更高效的读写操作。

常用构造函数:

  • BufferedReader(Reader reader):创建字符输入缓冲流。
  • BufferedWriter(Writer writer):创建字符输出缓冲流。

常用方法:

  • readLine():从输入流中读取一行文本。
  • write(String str):将字符串写入输出流。
  • close():关闭流。

序列化与反序列化

序列化(Serialization)

  1. 定义:序列化是将对象转换为字节流的过程,使得它可以被保存到文件、数据库或通过网络传输。
  2. 实现方式:在Java中,对象要进行序列化,必须实现java.io.Serializable接口。这个接口没有任何方法,它只是一个标记接口,表示该类可以被序列化。

反序列化(Deserialization)

  1. 定义:反序列化是将字节流重新转换为对象的过程,以便恢复原始对象的状态。
  2. 实现方式:与序列化一样,反序列化需要实现java.io.Serializable接口。通常,你需要使用ObjectInputStream来从字节流中读取对象。

        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("D:\\123.txt"));
        out.writeObject(new Student("张三",20));
        out.writeObject(new Student("李四",21));
        out.writeObject(new Student("王五",22));
        out.flush();
        out.close();
        ObjectInputStream in=new ObjectInputStream(new FileInputStream("D:\\123.txt"));
        try {
            Object o;
            while ((o = in.readObject()) != null) {
                System.out.println(o);
            }
        } catch (EOFException e) {
            // 达到输入流末尾,循环结束
            System.out.println("已到达输入流末尾");
        } catch (IOException | ClassNotFoundException e) {
            // 其他异常处理
            e.printStackTrace();
        }finally {
            in.close();
        }
        // System.out.println(o);
        // System.out.println(in.readObject());
        // System.out.println(in.readObject());

多线程

线程的六种状态

  1. 新建状态 (New):线程对象被创建但还没有调用 start() 方法时处于这个状态。在新建状态下,线程尚未启动执行。
  2. 可运行状态 (Runnable):线程处于可运行状态表示线程已经调用了 start() 方法,并且正在等待操作系统分配时间片执行。在可运行状态下,线程可能正在运行,也可能正在等待CPU的分配。
  3. 运行状态 (Running):线程被操作系统分配到CPU执行任务时处于运行状态。在运行状态下,线程的代码正在被执行。
  4. 阻塞状态 (Blocked):线程处于阻塞状态表示线程暂时无法继续执行,通常是因为某些条件没有满足。比如线程可能在等待某个锁的释放或等待某个输入/输出操作完成。
  5. 等待状态 (Waiting):线程处于等待状态表示线程正在等待其他线程的通知或等待一些特定条件满足。线程可以通过 wait()join() 等方法进入等待状态。
  6. 超时等待状态 (Timed Waiting):线程处于超时等待状态表示线程在等待一段时间后会自动唤醒。线程可以通过 sleep()wait(timeout)join(timeout) 等方法进入超时等待状态。

创建多线程的方法

//常用方法
start():启动线程并执行其run()方法。

run():线程的执行逻辑通常在run()方法中定义。

sleep(long millis):使线程休眠指定毫秒数,可以用于线程的定时操作。

yield():让出当前线程的CPU执行时间,给其他线程执行的机会。通常用于协调多个线程的执行顺序。

join():等待线程终止,可以用于等待其他线程完成后再继续执行。

interrupt():中断线程,向线程发送中断信号,线程可以检测到中断并做出相应的处理。

isInterrupted():检测线程是否被中断。

currentThread():获取当前线程的引用。

setName() 和 getName():设置和获取线程的名称。

setPriority() 和 getPriority():设置和获取线程的优先级。

isAlive():检测线程是否处于活动状态(已启动但未终止)。

wait() 和 notify() / notifyAll():线程间的等待和通知机制,用于线程间的协作。

getState():获取线程的状态(例如NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)。

setDaemon():将线程设置为守护线程(守护线程在主线程结束时会自动退出)。

setUncaughtExceptionHandler():设置未捕获异常的处理器。

getContextClassLoader() 和 setContextClassLoader():获取和设置线程的上下文类加载器。

这些方法提供了多线程编程所需的基本功能,可以实现线程的创建、启动、控制、同步和通信等。根据具体的需求,你可以选择合适的方法来编写多线程代码。需要注意的是,多线程编程需要小心处理线程同步和资源共享,以避免潜在的并发问题。

继承Thread类

创建一个继承自Thread类的子类,然后重写run()方法来定义线程的执行逻辑。通过创建该子类的实例并调用start()方法来启动线程。

class MyThread extends Thread {
    public void run() {
        // 线程执行的代码
    }
}
   MyThread thread = new MyThread();
   thread.start();

在多线程编程中,使用start()方法来启动一个新线程,而不是直接调用run()方法的原因在于线程的生命周期和并发执行。

  1. start()方法创建新线程

    调用start()方法会创建一个新的线程,并在新线程上执行run()方法中的代码。这意味着你可以同时执行多个线程,每个线程都在独立的执行路径上运行。

  2. run()方法是普通的方法

    如果直接调用myThread.run(),实际上并没有创建新线程。run()方法会在当前线程(通常是主线程)的上下文中执行,而不会创建新的线程。这样做就失去了多线程的优势,因为所有线程都在同一个执行路径上运行,无法实现并发。

  3. 并发性和线程管理

    使用start()方法来启动线程,线程的生命周期由Java虚拟机负责管理,包括线程的创建、执行、休眠、阻塞、等待和销毁。这使得线程能够更好地利用计算资源,并具有更高的并发性。

总之,start()方法是创建和启动新线程的标准方式,它允许多个线程并发执行。直接调用run()方法不会创建新线程,只是在当前线程中顺序执行run()方法的代码。因此,在多线程编程中,通常使用start()来启动新线程以实现并发执行。

实现Runnable接口

创建一个实现Runnable接口的类,然后将该类的实例传递给Thread类的构造函数。在实现的run()方法中定义线程的逻辑。

class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的代码
    }
}
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();

使用匿名内部类

可以使用匿名内部类来实现线程。这通常在需要一个简单的线程执行逻辑时很方便。

Thread thread = new Thread(new Runnable() {
    public void run() {
        // 线程执行的代码
    }
});
thread.start();

实现Callable接口,创建FutureTask对象

FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(futureTask);

class MyCallable implements Callable<String>{

    @Override
    public String call() throws Exception {
        return "小飞机的线程";
    }
}
synchronized

synchronized 是Java中用于实现多线程同步的关键字,它可以用于不同的地方来确保线程安全。以下是synchronized关键字的主要用法:

  1. 同步实例方法:使用synchronized关键字修饰实例方法,确保在同一对象实例上的不同线程不会同时执行该方法。例如:

    javaCopy codepublic synchronized void someMethod() {
        // 这个方法是线程安全的
    }
    
  2. 同步代码块:使用synchronized关键字来创建同步代码块,以确保只有一个线程可以同时进入这个代码块。语法如下:

    javaCopy codesynchronized (lockObject) {
        // 这里的代码是线程安全的
    }
    
    • lockObject 可以是任何对象,通常是用于共享资源的对象。
    • 进入同步块的线程会获取 lockObject 的锁,其他线程会在同步块外等待或阻塞,直到锁被释放。
  3. 静态同步方法:使用synchronized关键字修饰静态方法,确保在同一类上的不同对象实例上的不同线程不会同时执行该静态方法。例如:

    javaCopy codepublic static synchronized void staticMethod() {
        // 这个静态方法是线程安全的
    }
    
  4. 同步块中的锁对象:在同步代码块中,通常需要选择一个锁对象,该对象用于控制线程访问共享资源。锁对象的选择很重要,它应该是所有需要同步的线程都能访问到的对象。

    • 对于实例方法同步块:通常使用 this 关键字作为锁对象,即 synchronized (this) { ... }
    • 对于静态方法同步块:通常使用类的 Class 对象作为锁对象,即 synchronized (ClassName.class) { ... }

synchronized 关键字用于解决多线程并发访问共享资源时可能出现的竞争条件,确保线程安全。它是一种简单而有效的同步机制,但需要小心使用,以避免死锁和性能问题。在现代Java中,还有更高级的同步工具和库可供使用,如 java.util.concurrent 包中的工具,可以更灵活地处理多线程同步问题。

Lock
  1. Lock接口Lock 锁是一个接口,它有两个最常用的实现类,分别是ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock。最常用的是ReentrantLock,它提供了与synchronized类似的功能,但更加灵活。
  2. 获取锁:要使用Lock 锁,你首先需要获取锁。你可以使用lock()方法来获取锁。如果锁不可用,线程将被阻塞,直到锁可用为止。
  3. 释放锁:使用unlock()方法来释放锁。在你的代码完成后,应该始终确保释放锁,以允许其他线程获取锁。
  4. tryLock()方法Lock 锁还提供了tryLock()方法,它尝试获取锁,但如果锁不可用,它会立即返回,而不是阻塞线程。这对于实现超时控制非常有用。
  5. 条件变量Lock 锁提供了条件变量的支持,通过Condition 接口,你可以使用await()signal()方法来实现更高级的线程同步控制,例如等待某个条件满足后再继续执行。
  6. 可重入性Lock 锁是可重入的,这意味着同一线程可以多次获得同一个锁,而不会死锁。

使用Lock 锁的主要优势是它提供了更多的灵活性和精细控制,允许开发人员更好地管理线程同步。然而,它也更复杂一些,需要开发人员确保在正确的地方获取和释放锁,以避免死锁等问题。在大多数情况下,synchronized关键字足够满足需求,但在需要更多控制的情况下,Lock 锁是一个有用的工具。

线程的阻塞

在Java多线程编程中,线程可以通过调用不同的方法进入阻塞状态。以下是一些常见的使线程进入阻塞状态的方法以及它们是否会释放资源的情况:

wait() 和 sleep()的区别?

二者都会让线程进入阻塞状态,有以下区别

  1. wait是Object的方法 sleep是Thread的方法

  2. wait会立即释放锁 sleep不会释放锁

  3. wait后线程的状态是Watting sleep后线程的状态为 Time_Waiting

  4. Thread.join()方法

    • 进入阻塞状态原因:线程调用另一个线程的 join() 方法时,它会进入等待阻塞状态,等待被调用的线程执行完成。
    • 是否释放资源:是的,线程会释放任何占有的对象锁资源。
  5. I/O操作

    • 进入阻塞状态原因:线程执行某些I/O操作,如读写文件、网络通信等,当操作需要等待数据传输或文件读写完成时,线程会进入阻塞状态。
    • 是否释放资源:通常情况下,线程会释放占有的对象锁资源,但不会释放I/O资源,例如文件句柄。
  6. 等待线程加入

    • 进入阻塞状态原因:当一个线程调用另一个线程的 Thread.join() 方法等待其加入(执行完成)时,等待的线程会进入阻塞状态。
    • 是否释放资源:是的,线程会释放任何占有的对象锁资源。

需要注意的是,不同情况下线程进入阻塞状态的原因和资源释放方式不同。正确处理阻塞状态可以避免死锁和提高多线程程序的效率。通常,线程会在等待的同时释放占有的对象锁资源,以允许其他线程获取锁。但在某些情况下,如I/O操作,线程会进入阻塞状态但不会释放底层资源,因此在编写多线程程序时需要谨慎处理这些情况。

在Java多线程编程中,有一些情况下线程不会释放资源,即使它进入了阻塞状态。以下是一些常见情况:

  1. I/O操作:当线程执行I/O操作时,例如读取文件、网络通信等,线程会阻塞等待I/O操作完成。在这种情况下,线程不会释放底层的I/O资源,例如文件句柄或网络连接。这意味着其他线程无法访问相同的资源,直到阻塞的线程完成I/O操作。
  2. synchronized锁:当线程获得了某个对象的锁(使用synchronized关键字),其他线程需要等待该锁的释放。即使等待的线程进入了阻塞状态,但它不会主动释放锁资源,只有持有锁的线程在退出synchronized代码块或方法时才会释放锁。
  3. Thread.yield()方法Thread.yield()方法使线程放弃当前的CPU时间片,从运行状态变为就绪状态,让其他就绪状态的线程有机会运行。线程不会释放对象锁资源。

这些情况下,线程虽然进入了阻塞状态,但不会主动释放占用的资源,这可能导致资源竞争和性能问题。因此,在编写多线程程序时,需要仔细考虑这些情况,以确保资源的合理管理和避免死锁等问题。

ArrayBlockingQueue<>泛型类

线程池

线程池的好处

  1. 降低资源消耗,通过池化思想,减少创建线程和销毁线程的消耗,控制资源
  2. 提高响应速度,任务到达时,无需创建线程即可运行
  3. 提供更多更强大的功能,可扩展性高
public ThreadPoolExecutor(int corePoolSize,							
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
 
}

范围描述
corePoolSize池中保留的核心线程数,即使它们处于空闲状态,除非allowCoreThreadTimeOut设置为 true。这是池将始终维护的最小线程数。
maximumPoolSize池中允许的最大线程数。当活动线程数超过此值并且工作队列已满时,线程池将开始根据拒绝策略拒绝新任务。
keepAliveTime如果当前线程数大于 corePoolSize,则多余的空闲线程在终止之前等待新任务的时间。
unit参数的时间单位keepAliveTime,例如毫秒、秒、分钟等。
workQueue用作任务队列的实现BlockingQueue,用于保存挂起的任务,直到它们被工作线程执行。当所有线程都忙时,该队列充当存储任务的缓冲区。
threadFactory用于创建新线程的工厂。它提供了自定义线程池创建的线程的灵活性,例如命名、优先级和守护进程状态。
handler被拒绝任务的处理程序。当线程池无法接受新任务时,此处理程序定义处理被拒绝任务的策略,例如中止、丢弃或在调用者线程中执行它们。
原子性

所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。

乐观锁与悲观锁

**悲观锁(Pessimistic Lock):

就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

但是在效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,如果已经锁定了一个线程A,其他线程就必须等待该线程A处理完才可以处理

乐观锁(Optimistic Lock):

就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作),乐观锁适用于多读的应用类型,这样可以提高吞吐量

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最常用的。详细说明

CAS自旋锁

AtomicInteger*原理自旋锁+CAS算法

CAS算法

有3个操作数(内存值V,旧的预期值A,要修改的值B)

当旧的预期值A==内存值此时修改成功,将V改为B

当旧的预期值A!=内存值此时修改失败,不做任何操作

并重新获取现在的最新值(这个重新获取的动作就是自旋)

阻塞队列(Blocking Queue)

ArrayBlockingQueue 是 Java 中的一个阻塞队列(Blocking Queue)实现,它基于数组的数据结构,具有固定的容量。这意味着一旦队列达到最大容量,后续的插入操作会被阻塞,直到队列中有空间可用。

以下是 ArrayBlockingQueue 常用方法的介绍:

  1. 构造方法:

    • ArrayBlockingQueue(int capacity):创建具有指定容量的 ArrayBlockingQueue,容量必须是正整数。
  2. 插入元素:

    • add(E e):向队列尾部插入元素,如果队列已满,抛出 IllegalStateException 异常。
    • offer(E e):向队列尾部插入元素,如果队列已满,返回 false
    • put(E e):向队列尾部插入元素,如果队列已满,线程将被阻塞直到队列有空间。
  3. 获取元素:

    • remove():移除并返回队列头部的元素,如果队列为空,抛出 NoSuchElementException 异常。
    • poll():移除并返回队列头部的元素,如果队列为空,返回 null
    • take():移除并返回队列头部的元素,如果队列为空,线程将被阻塞直到队列有元素可用。
  4. 检查队列状态:

    • size():返回队列中的元素数量。
    • isEmpty():检查队列是否为空。
    • isFull():检查队列是否已满(注意,ArrayBlockingQueue 本身没有提供此方法,但可以通过比较 size() 和队列容量来实现)。

    ArrayBlockingQueue 是多线程环境下非常有用的数据结构,它可以用于实现生产者-消费者模式等场景,确保线程之间的安全通信和协作。当使用阻塞队列时,需要谨慎处理可能出现的阻塞情况,以免程序陷入死锁或无法正常结束。

cas (compare and swap)

比较并交换

为变量赋值时,从内存中读取到的值v,获取到要交换的新值n,执行 compareAndSwap()方法时,比较v和当前内存中的值是否一致,如果一致则将n和v交换,如果不一致,则自旋重试。

cas底层是cpu层面的,即不使用同步锁也可以保证操作的原子性。

无锁的效率是要高于之前的锁的,由于无锁不会涉及线程的上下文切换

cas是乐观锁的思想,sychronized是悲观锁的思想

cas适合很少有线程竞争的场景,如果竞争很强,重试经常发生,反而降低效率

ABA问题

cas存在ABA问题,即比较并交换时,如果原值为A,有其他线程将其修改为B,在有其他线程将其修改为A。

此时实际发生过交换,但是比较和交换由于值没改变可以交换成功

解决方式

AtomicStampedReference/AtomicMarkableReference

上面两个类解决ABA问题,原理就是为对象增加版本号,每次修改时增加版本号,就可以避免ABA问题

或者增加个布尔变量标识,修改后调整布尔变量值,也可以避免ABA问题。说明

反射

获取反射中的Class对象有三种常见的方法:

  1. 使用.class属性
    • 用于获取类的Class对象。在类名后面加上.class属性,即可获得该类的Class对象。
    • 示例:Class<?> clazz = MyClass.class;
  2. 使用.getClass()方法
    • 可以通过任何对象来获取其对应的Class对象。
    • 示例:Class<?> clazz = myObject.getClass();
  3. 使用Class的静态方法forName()
    • 使用Class类的静态方法forName(),提供类的全限定名,可以在运行时获取Class对象。
    • 示例:Class<?> clazz = Class.forName("com.example.MyClass");

Java反射机制基础知识点:

  1. Class对象:每个类都有一个对应的Class对象,它包含了该类的结构信息。你可以通过类名、对象实例或Class.forName()方法来获取Class对象。
  2. 获取类的信息:通过Class对象,你可以获取类的信息,如类名、包信息、父类、接口、构造方法、字段和方法等。
  3. 创建对象:你可以使用Class对象的newInstance()方法创建类的实例,前提是类有一个无参构造方法。
  4. 访问和修改字段:使用Field对象可以获取和设置类的字段的值,包括私有字段。
  5. 调用方法:使用Method对象可以调用类的方法,包括私有方法。
  6. 获取构造方法:通过Class对象可以获取构造方法,然后使用构造方法创建对象。
  7. 动态加载类:可以在运行时加载新的类,通常用于插件系统或动态模块加载。
  8. 数组和泛型:反射支持操作数组和泛型类型。
方法作用示例
Class.forName(String className)加载并返回指定类的Class对象。Class<?> clazz = Class.forName("com.example.MyClass");
Class.getDeclaredMethods()返回该类的所有声明方法的数组,包括私有方法。Method[] methods = clazz.getDeclaredMethods();
Class.getDeclaredFields()返回该类的所有声明字段的数组,包括私有字段。Field[] fields = clazz.getDeclaredFields();
Class.getConstructor(Class<?>... parameterTypes)获取指定参数类型的构造函数。Constructor<?> constructor = clazz.getConstructor(int.class, String.class);
Class.newInstance()使用无参构造函数创建类的新实例。Object instance = clazz.newInstance();
Method.invoke(Object obj, Object... args)调用类的方法。Object result = method.invoke(instance, arg1, arg2);
Field.get(Object obj)获取类的字段的值。Object value = field.get(instance);
Field.set(Object obj, Object value)设置类的字段的值。field.set(instance, newValue);
Method.setAccessible(boolean flag)允许或禁止访问私有方法。需要在调用私有方法之前设置。method.setAccessible(true);

注解

在Java中,注解(Annotation)是一种元数据(metadata)形式,它提供了关于程序元素(如类、方法、字段等)的附加信息。注解通常用于在代码中添加标签或描述性信息,以便在编译、运行时或工具处理过程中进行识别和处理。注解的使用可以增强代码的可读性、可维护性和可重用性。

  1. @符号:注解以 "@" 符号开头,紧接着是注解的名称。
  2. 元注解:Java提供了一些内置的元注解,用于定义和配置自定义注解。元注解包括 @Retention@Target@Documented@Inherited 等。
  3. 自定义注解:除了内置注解外,开发人员可以创建自己的自定义注解,通过 @interface 关键字定义。
  4. 注解的参数:注解可以具有参数,这些参数可以是基本数据类型、字符串、枚举类型或其他注解类型。
  5. 目标范围:注解可以用于不同的程序元素,例如类、方法、字段、参数等。这是通过元注解 @Target 来指定的。
  6. 保留策略:注解可以在源代码、编译时和运行时保留不同的级别,这是通过元注解 @Retention 来指定的。

XML

先引入dom4j.jar文件

public static void main(String[] args) throws DocumentException {
    //1.获取一个解析器对象
        SAXReader sr = new SAXReader();
    //2.利用解析器把xml文件加载到内存中,并返回一个文档对象
        Document document = sr.read(new File("D:\\java文件包\\test\\Person.xml"));
      //3.获取到根标签
        Element rootElement = document.getRootElement();
     //4.通过根标签来获取person标签
        List<Element> ProElements = rootElement.elements("Person");
    //5.遍历集合,得到每一个person标签
        for (Element element : ProElements) {
            //获取属性
            Attribute attribute = element.attribute("id");
            //获取属性值
            String id = attribute.getValue();
            System.out.println("id="+id);\
            //获取标签
            Element nameEle = element.element("name");
            //获取这个标签的标签体内容
            System.out.println("name="+nameEle.getText());
            Element anInt = element.element("age");
            System.out.println("age="+anInt.getText());
        }
    }

内部类

在Java中,内部类是定义在另一个类内部的类,它具有特定的作用域和访问权限规则。内部类允许在一个类内部创建另一个类,这对于实现一些特定的设计模式或封装复杂逻辑非常有用。内部类的语法格式有以下几种:

  1. 成员内部类(Member Inner Class): 成员内部类是定义在另一个类的内部的普通类,它可以访问外部类的成员,包括私有成员。

    public class Outer {
        private int outerVar;
    
        public class Inner {
            public void innerMethod() {
                System.out.println("Inner class method: " + outerVar);
            }
        }
    }
    
  2. 静态内部类(Static Nested Class): 静态内部类是定义在另一个类的内部的静态类,它不依赖于外部类的实例,可以通过外部类的类名直接访问。

  3. 局部内部类(Local Inner Class): 局部内部类是定义在方法或代码块内部的类,它的作用域仅限于方法或代码块内部,无法被外部访问。

    public class Outer {
        public void outerMethod() {
            class LocalInner {
                public void localInnerMethod() {
                    System.out.println("Local inner class method");
                }
            }
            LocalInner inner = new LocalInner();
            inner.localInnerMethod();
        }
    }
    
  4. 匿名内部类(Anonymous Inner Class): 匿名内部类是一种没有显式类名的内部类,通常用于创建实现某个接口或继承某个类的匿名对象。

函数式编程

Lambda

lamdba表达式使用前提:有一个接口,接口中有且仅有一个抽象方法

lambda表达式省略规则:

  • 小括号内的参数类型可以省略
  • 如果小括号内有且仅有一个参,则小括号可以省略
  • 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号

Stream

Java中的Stream是一种用于操作集合数据的高级抽象,你以声明性的方式处理数据,而不需要编写传统的循环和条件语句。Stream API引入了函数式编程的思想,可以更容易地进行过滤、映射、排序、聚合等操作。

Stream介绍:

  • Stream是Java 8引入的API,用于处理集合和数组数据。
  • Stream提供了一种便捷的方式来操作数据,支持链式调用,可以将多个操作组合在一起。
  • Stream不会直接修改原始数据,而是生成一个新的Stream,因此它是无状态的。
  • Stream可以并行执行操作,提高性能。

常用Stream方法:

方法描述
filter(Predicate<T> predicate)过滤元素,保留符合条件的元素。
map(Function<T, R> mapper)将元素映射为新的值,生成一个新的Stream。
flatMap(Function<T, Stream<R>> mapper)将元素映射为Stream并将多个Stream合并成一个新的Stream。
distinct()去除重复的元素。
sorted()对元素进行排序,默认使用自然顺序。
sorted(Comparator<T> comparator)使用指定的比较器对元素进行排序。
forEach(Consumer<T> action)对每个元素执行指定操作。
collect(Collector<T, A, R> collector)将流中的元素收集为一个容器,如List、Set、Map等。
count()返回流中元素的数量。