学习菜鸟教程!
Java编程语言有许多显著的特征,使其成为广泛应用的语言。以下是Java的五个最主要特征:
Java编写的程序可以在不同的操作系统上运行,而不需要修改源代码。这个特性归功于Java虚拟机 (Java Virtual Machine, JVM)。Java代码首先被编译成平台无关的字节码 (Bytecode),然后通过JVM在不同的平台上解释和执行。这一特性使Java成为一种“编写一次,随处运行”的语言。
Java是一种严格的面向对象的编程语言。它的设计基于对象和类的概念,促进了代码的可重用性和模块化。主要的OOP特性包括:
Java具有垃圾收集 (Garbage Collection) 功能,它可以自动管理内存的分配和回收。开发者不需要手动释放内存,减少了内存泄漏和其他内存管理问题的风险。这使得Java程序在内存管理方面更容易和更安全。
Java提供了丰富且功能强大的标准库 (Java Standard Library),它涵盖了从数据结构、网络编程、数据库连接、图形用户界面 (GUI) 到多线程等各种应用。这些库大大简化了开发过程,开发者可以专注于应用逻辑而不是底层实现。
Java从设计之初就强调安全性。它的安全机制包括:
这些特性使Java成为开发安全、稳定和跨平台应用的理想选择。
Java应用程序结构包括包、类和接口、方法、成员变量、构造函数、主方法、异常处理、文件结构、外部依赖和配置文件。理解这些组件及其组织方式是构建和维护Java应用程序的基础。
package com.example.myapp;
,使用的时候要用如此,import java.util.List;
,相对的python的import:from mypackage import submodule
,他们的目的都是为了,便于组织,唯一的命名,和import方便public static void main(String[] args)
这是 main 方法的参数,它是一个 String 类型的数组,用于接收从命令行传递给程序的参数。这些参数在程序启动时传递给 main 方法,可以在程序中使用它们。
运行:java MainExample arg1 arg2
为了代码的清晰和组织性,建议将每个公共类放在单独的文件中。并且这个文件名必须与类名匹配。
对象的广泛使用
面向对象编程(OOP)原则
FP
在 Java 编程语言中,变量可以分为两种主要类型:基本类型(Primitive Types)和引用类型(Reference Types)。
基本类型基本以小写字母开头,引用类型基本以大写字母开头。
基本类型是 Java 中最简单的数据类型。它们存储的是实际的值,而不是指向值的引用。Java 提供了 8 种基本数据类型,每一种类型都有固定的大小和范围:
根据bit位数:
byte
:8位,表示范围为 -128 到 127。short
:16位,表示范围为 -32,768 到 32,767。int
:32位,表示范围为 -2^31 到 2^31 - 1。long
:64位,表示范围为 -2^63 到 2^63 - 1。float
:32位,单精度浮点数。double
:64位,双精度浮点数。char
:16位,表示一个单一的 Unicode 字符,范围为 0 到 65,535。boolean
:表示真 (true
) 或假 (false
) 两个值。这些基本类型都具有直接存储的值,并且内存消耗固定且较小,因此它们是效率最高的数据存储方式之一。
引用类型用于存储对象的引用,而不是对象的值本身。Java 中的引用类型包括:
String
是 Java 中的一个类,当你创建一个 String
类型的变量时,它实际上是对 String
对象的引用。String text = "Hello, World!";
List<String> list = new ArrayList<>();
int[] numbers = {1, 2, 3, 4, 5};
enum Day { MONDAY, TUESDAY, WEDNESDAY }
Day today = Day.MONDAY;
引用类型变量实际存储的是对象在内存中的地址,而不是对象本身。对象的操作通过这些引用来间接地访问实际的对象。
单引号和双引号在 Java 中的不同用途和规则是编写和调试 Java 程序的重要基础。单引号用于字符常量,而双引号用于字符串常量,它们分别对应 char 和 String 类型。
int
默认值是 0
,boolean
默认值是 false
)。null
,表示它们没有指向任何对象。以下示例展示了基本类型和引用类型的使用:
public class Main {
public static void main(String[] args) {
// 基本类型
int num = 10; // 整型基本类型
double pi = 3.14; // 双精度浮点型基本类型
// 引用类型
String greeting = "Hello, World!"; // String 类
int[] numbers = {1, 2, 3}; // 数组类型
MyClass obj = new MyClass(); // 自定义类的对象
System.out.println("Number: " + num);
System.out.println("Pi: " + pi);
System.out.println("Greeting: " + greeting);
System.out.println("First number in array: " + numbers[0]);
System.out.println("Object reference: " + obj);
}
}
class MyClass {
// 自定义类
}
# 更新
brew update
# 搜索
brew search openjdk
# 开始安装
brew install openjdk@17
# 根据安装后的提示,run了如下命令
If you need to have openjdk@17 first in your PATH, run:
echo 'export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"' >> ~/.zshrc
For compilers to find openjdk@17 you may need to set:
export CPPFLAGS="-I/opt/homebrew/opt/openjdk@17/include"
# 使更新生效
source ~/.zshrc
# 验证版本
java -version
# 确认安装路径
brew --prefix openjdk@17
# /opt/homebrew/opt/openjdk@17
# found the home
/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
编辑VScode的配置文件setting.json
"java.configuration.runtimes": [
{
"name": "JavaSE-17",
"path": "/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home",
"default": true
}
]
为了配置项目根目录,我配置了如下设置:
"java.project.sourcePaths": ["src"],
在 Java 中,通过 javac
编译后生成的文件通常是字节码文件(bytecode files),而不是二进制文件。Java 编译器 (javac
) 将 Java 源代码编译成 Java 字节码,而不是将其编译成本地机器的二进制代码。
Java 字节码是一种中间代码(intermediate code),它不直接运行在计算机的硬件上,而是运行在 Java 虚拟机(JVM)上。这种中间代码的好处是跨平台性,即你可以将同样的字节码文件在不同的操作系统上运行,只要这些系统都安装了相同版本的 Java 运行时环境(JRE)或 Java 开发工具包(JDK)。
Java 字节码文件的扩展名是 .class
,每个 .class
文件对应一个 Java 类。例如,编译后生成的 Main.class
文件就是一个 Java 字节码文件。
运行方法:
javac Main.java
java Main
复杂一点的:
编译代码
从项目的根目录(project_root
)下执行编译命令。编译器会根据 src
目录下的包结构生成对应的类文件。
导航到项目根目录。假设你的项目目录是 project_root
:
cd /path/to/project_root
执行编译命令:
javac -d out src/main/MainAccount.java src/bank/BankAccount.java
这个命令的解释:
javac
是 Java 编译器命令。-d out
表示将编译后的类文件输出到 out
目录,并保持包结构。src/main/MainAccount.java
和 src/bank/BankAccount.java
是需要编译的 Java 源文件。编译完成后,目录结构应为:
project_root/
├── out/
│ ├── MainAccount.class
│ └── bank/
│ └── BankAccount.class
├── src/
│ ├── MainAccount.java
│ └── bank/
│ └── BankAccount.java
运行代码
编译完成后,你可以运行 MainAccount
类。确保你在项目的根目录下执行运行命令:
在终端中:
java -cp out main.MainAccount
这个命令的解释:
java
是 Java 运行时命令。-cp out
指定类路径为 out
目录,Java 虚拟机会从这个目录加载编译后的类文件。main.MainAccount
是类的全限定名,main
是包名,MainAccount
是类名。输出结果:
运行后,你应该看到如下输出:
Account number: 78986
Account holder: Saally
Account balance: 1.0E12
是静态方法的意思,表明这个member,它属于类,不属于实例,如果一个函数有static,那么可以在类中直接使用该方法,如果没有static关键字,则需要对类进行实例化。
Python中也有staticmethod修饰符。表示就是可以直接靠类使用的方法。
继承其他的class,对于变量可以直接使用,对于方法需要是public/protected属性。
构造方法重载指的是在一个类中可以定义多个构造方法,它们具有相同的名字(类的名字),但参数列表不同(参数的类型、数量或顺序不同)。通过这种方式,可以根据不同的需求初始化对象,提供灵活性和方便性。
相当于Python中的初始化函数,但是Python的初始化函数不能有多个,只能靠类方法等实现。
++x
:先自增,再使用。x++
:先使用,再自增。
对象的比较:boolean same = s.equals(s1);
也就是使用自身的方法
字符串取切片区间:String substring = s.substring(1, 2);
区间和Python一样也是包含左边不包含右边
以某字符为prefix:boolean startswith = s.startsWith("H");
长度:s.length()
s.charAt(3)
返回index位于3的字符
Cast,类型转换:
long l = 123;
int x = (int) l;
double d = 1.2;
float f = (float) d;
在 Java 中,字符类型 char
使用 16 位无符号整数表示 Unicode 字符,这意味着它可以表示的最小值是 0
,最大值是 65535
。如果超过了这个范围,char
类型就会溢出(overflow)。
char
类型的范围:
char
类型在 Java 中表示一个 16 位的无符号整数。0
到 65535
,也就是 2^16 - 1
。char
类型,当值达到 65535
时,再加 1
,就会超出它的最大值,发生溢出。char
类型的最大值 65535
加 1
,它就会溢出。char
类型是无符号的,所以它不能表示负数。溢出后,值会从头开始,也就是从 0
开始。65535 + 1
在 char
类型下会变成 0
。public class CharOverflowExample {
public static void main(String[] args) {
char maxChar = 65535; // char 的最大值
System.out.println("Initial maxChar value: " + (int) maxChar); // 输出 65535
// 对 maxChar 加 1
maxChar++;
// 输出溢出后的值
System.out.println("maxChar after increment: " + (int) maxChar); // 输出 0
}
}
栈内存用于存储局部变量、方法调用和方法参数。它是一个LIFO(Last In, First Out)结构,意味着最后一个被压入栈的元素最先被弹出。栈的主要特点和用途包括:
public class StackExample {
public static void main(String[] args) {
int a = 5; // 局部变量a
int b = 10; // 局部变量b
int result = add(a, b); // 方法调用,result存储返回值
System.out.println(result);
}
public static int add(int x, int y) {
int sum = x + y; // 局部变量sum
return sum; // 返回sum
}
}
在上述代码中,a
、b
、x
、y
和 sum
都是存储在栈上的局部变量。add
方法调用时,会在栈上创建一个新的栈帧来存储其参数和局部变量。
堆内存用于动态分配存储在Java中的所有对象和类实例。与栈不同,堆内存的管理更为复杂和灵活。堆的主要特点和用途包括:
public class HeapExample {
public static void main(String[] args) {
Person person = new Person("John", 30); // 创建一个Person对象
System.out.println(person.getName() + " is " + person.getAge() + " years old.");
}
}
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;
}
}
在上述代码中,Person
对象存储在堆上,name
和 age
是该对象的实例变量,它们也存储在堆上。
特性 | 栈(Stack) | 堆(Heap) |
---|---|---|
用途 | 存储局部变量、方法调用和方法参数 | 存储所有对象和数组 |
内存管理 | 自动管理(LIFO) | 由垃圾回收器自动管理 |
访问速度 | 快(由于LIFO结构) | 相对较慢(由于需要动态分配和垃圾回收) |
生命周期 | 与方法调用周期一致 | 对象可在方法结束后继续存在 |
大小限制 | 较小,受限于栈大小(可能导致StackOverflowError) | 较大,适合存储大量和长期存在的数据 |
内存释放 | 方法结束时自动释放 | 通过垃圾回收机制自动释放 |
理解栈和堆在Java中的角色和区别,对于有效地管理内存和优化应用程序性能非常重要。在编写代码时,注意局部变量和对象的使用方式,可以帮助避免常见的内存管理问题,如栈溢出和内存泄漏。
String不可变immutable,这意味着,一个字符串对象的值是不可改变的,因为任何新的字符串值会被存储在一个分开的新的对象中,变量会指向这个新的对象的引用。
在Java中,字符串(String
类)是线程安全的。线程安全性意味着多个线程可以同时访问同一个对象,而不会导致数据不一致或其他并发问题。Java中的String
类具有以下几个特性,使其成为线程安全的:
不可变性(Immutability):
String
对象是不可变的。一旦创建了一个String
对象,它的值就不能被改变。任何对字符串的操作(如拼接、替换等)都会生成一个新的String
对象,而不是修改原来的对象。这种不可变性确保了在多线程环境下,一个线程对String
对象的修改不会影响到其他线程。内部实现:
String
类的底层实现使用了一个final
的字符数组来存储字符串的值。这个字符数组在String
对象创建后也不能被修改。即使多个线程同时访问这个字符数组,也不会有并发问题。常量池:
尽管String
本身是线程安全的,但在多线程环境下操作字符串时,仍需要注意一些其他问题。例如,如果在多个线程中频繁地拼接字符串,最好使用StringBuilder
或StringBuffer
类来代替String
。其中,StringBuilder
是非线程安全的,但性能较高,而StringBuffer
是线程安全的,可以在多线程环境中使用。他们操作起来就像是在操作数组。
如果使用equals(),则是他们的content被比较。当字符串在常量池中指向同一个对象的时候,==可以得到True的结果。
java.time.LocalDate
java.time.LocalTime
java.time.LocalDateTime
java.time.ZonedDateTime
set by ZoneIdjava.time.Duration
:秒和微秒java.time.Period
:年月日加减区间:plus/minus duration/period
.now()
, .of()
, .parse()
.getYear()
, .getDayOfWeek()
, and so on.minusWeeks()
, .plusDays()
, and so on
java.time.format.DateTimeFormatter
: .ofPattern("MM/dd/yyyy")
.format()
.parse(<string>, <the instance of the formatter>)
java.time.format.DateTimeFormatterBuilder
: 类似于 String 的 Builder,可以进行append等类似列表的操作,很神奇
Java的面向对象编程(OOP)有四大核心特性:封装、继承、多态和抽象。这些特性使得Java程序具有模块化、可维护性和可扩展性。
封装是将对象的属性和方法封装在一个类中,通过访问控制(如private, protected, public)来限制对这些属性和方法的直接访问。这样可以保护数据不被随意修改,同时也隐藏了对象的内部实现细节。
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
继承是通过从已有的类(称为父类或超类)中创建一个新的类(称为子类或派生类),使子类可以继承父类的属性和方法,并可以新增自己的属性和方法。继承实现了代码的复用和扩展。
public class Animal {
public void eat() {
System.out.println("This animal eats food.");
}
}
public class Dog extends Animal {
public void bark() {
System.out.println("The dog barks.");
}
}
多态是指同一操作在不同对象上具有不同表现形式的能力。多态性通过方法重载和方法重写实现。它允许对象在不同的上下文中以不同的方式进行响应。
public class Animal {
public void makeSound() {
System.out.println("Some generic animal sound");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Bark");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.makeSound(); // 输出:Bark
myCat.makeSound(); // 输出:Meow
}
}
抽象是指将对象的复杂实现隐藏起来,只保留对象的必要特性和行为。通过抽象类和接口,可以定义对象的抽象行为。
abstract class Animal {
public abstract void makeSound();
public void sleep() {
System.out.println("This animal sleeps.");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Bark");
}
}
class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
myDog.makeSound(); // 输出:Bark
myDog.sleep(); // 输出:This animal sleeps.
}
}
通过这四大特性,Java实现了面向对象编程的理念,帮助开发者构建更为结构化、模块化和可维护的代码。
类型安全,可读性强,内存运算效率高
每个枚举都是通过 Class 在内部实现的,且所有的枚举值都是 public static final 的。
Class
)在内部实现的在Java中,每个枚举类型在编译时都会自动生成一个对应的类。这意味着每个枚举类型实际上是一个类,这个类继承自java.lang.Enum
。例如,定义如下的枚举:
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}
编译后会生成一个类似以下结构的类:
public final class Day extends Enum<Day> {
public static final Day SUNDAY = new Day("SUNDAY", 0);
public static final Day MONDAY = new Day("MONDAY", 1);
public static final Day TUESDAY = new Day("TUESDAY", 2);
public static final Day WEDNESDAY = new Day("WEDNESDAY", 3);
public static final Day THURSDAY = new Day("THURSDAY", 4);
public static final Day FRIDAY = new Day("FRIDAY", 5);
public static final Day SATURDAY = new Day("SATURDAY", 6);
private static final Day[] VALUES = {SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY};
private Day(String name, int ordinal) {
super(name, ordinal);
}
public static Day[] values() {
return VALUES.clone();
}
public static Day valueOf(String name) {
for (Day day : VALUES) {
if (day.name().equals(name)) {
return day;
}
}
throw new IllegalArgumentException("No enum constant " + name);
}
}
public static final
枚举类型的每个枚举常量都是public static final
的,这意味着:
Day.MONDAY
。每个枚举常量实际上是枚举类型的一个实例。由于这些常量是static
的,它们在类加载时就被创建并初始化。因此,可以在任何地方通过类名直接访问它们。
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}
public class EnumExample {
public static void main(String[] args) {
// 使用for-each循环遍历所有的枚举常量
for (Day day : Day.values()) {
System.out.println(day);
}
}
}
在Java编程语言中,Interface
(接口)是一种抽象类型,定义了一组方法,但不提供这些方法的实现。接口指定了类必须遵循的协议,它是实现多态性和解耦代码的关键机制。
以下是Java接口的一些关键特性:
public
和abstract
,不包含方法体。default
方法和static
方法。default
方法提供了方法的默认实现,static
方法则是接口中的静态方法,private
是只能在接口内部使用的方法,隐藏方法的内部结构public static final
。即,它们是公共的、静态的、不可变的常量。implements
关键字来实现一个接口,并且必须提供接口中所有抽象方法的实现。Serializable
接口。这些被称为标记接口,用于表示类具有某种属性。下面是一个简单的接口示例,以及一个实现该接口的类:
// 定义一个接口
public interface Animal {
// 抽象方法,没有花括号
void eat();
void sleep();
}
// 实现该接口的类
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog is eating");
}
@Override
public void sleep() {
System.out.println("Dog is sleeping");
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat();
dog.sleep();
}
}
在这个例子中,Animal
接口定义了两个抽象方法eat
和sleep
。Dog
类实现了Animal
接口,并提供了这些方法的具体实现。在main
方法中,创建了一个Dog
对象并调用了其eat
和sleep
方法。
接口在Java编程中有许多应用场景,包括:
接口冲突:如果一个类有两个接口,两个接口有相同的default
方法,会产生冲突conflict,解决它必须在类中override
这个方法,让类有定义的方法,也就是覆盖这两个冲突的默认方法。
在Java中,抽象类和抽象方法是面向对象编程中的重要概念,它们用于定义通用行为的框架,而不提供具体实现。以下是它们的定义和用法:
抽象类是不能被实例化的类,它通常包含一个或多个抽象方法(没有方法体的方法),以及可以包含具体的方法(有方法体的方法)。抽象类提供了一种定义类层次结构和共享代码的机制。
abstract
定义。抽象方法是没有方法体的方法,仅声明方法签名。它们必须在抽象类中定义,并且必须在非抽象子类中实现。
abstract
定义。// 定义一个抽象类
public abstract class Animal {
// 抽象方法
public abstract void eat();
public abstract void sleep();
// 具体方法
public void breathe() {
System.out.println("Animal is breathing");
}
}
// 实现抽象类的子类
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog is eating");
}
@Override
public void sleep() {
System.out.println("Dog is sleeping");
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat();
dog.sleep();
dog.breathe();
}
}
在这个例子中:
Animal
是一个抽象类,包含两个抽象方法eat
和sleep
,以及一个具体方法breathe
。Dog
类继承了Animal
类,并实现了所有的抽象方法。main
方法中,创建了一个Dog
对象,并调用了其eat
、sleep
和breathe
方法。抽象类和抽象方法在Java编程中有许多应用场景,包括:
通过使用抽象类和抽象方法,Java开发者可以创建更灵活和可维护的代码结构,实现代码的复用和扩展。
Java的泛型(Generics)和集合(Collections)是Java编程语言中两个重要的概念,广泛用于编写类型安全和灵活的代码。
泛型是一种编程语言的特性,允许在定义类、接口和方法时使用类型参数。它们使得代码可以用于不同的数据类型,而不必重新编写代码,同时提供了编译时类型检查,提高了代码的安全性和可维护性。
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.set(10);
System.out.println("Integer Value: " + integerBox.get());
Box<String> stringBox = new Box<>();
stringBox.set("Hello Generics");
System.out.println("String Value: " + stringBox.get());
}
}
public class GenericsExample {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"A", "B", "C", "D", "E"};
printArray(intArray);
printArray(strArray);
}
}
public class GenericsExample {
public static <T extends Number> void printNumber(T number) {
System.out.println("Number: " + number);
}
public static void main(String[] args) {
printNumber(10); // Integer
printNumber(10.5); // Double
// printNumber("10"); // 编译错误,String不是Number的子类
}
}
集合框架(Collections Framework)是Java提供的一组类和接口,用于存储和操作数据集合。它们提供了对数据结构的高效操作,包括列表(List)、集合(Set)、队列(Queue)和映射(Map)。
ArrayList
、LinkedList
。List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String s : list) {
System.out.println(s);
}
HashSet
、LinkedHashSet
、TreeSet
。Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("A"); // 重复元素不会被添加
for (String s : set) {
System.out.println(s);
}
LinkedList
、PriorityQueue
。Queue<String> queue = new LinkedList<>();
queue.add("A");
queue.add("B");
queue.add("C");
System.out.println(queue.poll()); // A
System.out.println(queue.poll()); // B
HashMap
、LinkedHashMap
、TreeMap
。Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("A", 3); // 键"A"的值被更新
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
Collections类提供了许多静态方法,用于操作或返回集合。常用方法有:
sort(List<T> list)
: 对列表进行排序。shuffle(List<?> list)
: 对列表进行随机排序。reverse(List<?> list)
: 反转列表中的元素顺序。List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Collections.shuffle(numbers);
System.out.println("Shuffled List: " + numbers);
Collections.sort(numbers);
System.out.println("Sorted List: " + numbers);
Collections.reverse(numbers);
System.out.println("Reversed List: " + numbers);
通过使用泛型和集合,Java开发者可以编写更灵活、高效和易于维护的代码。
在Java中,Comparable
和 Comparator
是用于对象比较和排序的两个重要接口。
Comparable
接口是Java类库提供的一种内部比较器接口,它允许实现了该接口的类的对象自行进行比较。实现了 Comparable
接口的类必须实现 compareTo()
方法,该方法返回一个整数值,用于表示对象的顺序关系。
方法签名:
public interface Comparable<T> {
int compareTo(T o);
}
用途:
Comparable
接口后,它的对象可以通过 Collections.sort()
方法进行排序。Comparable
接口的类的对象可以作为元素存储在 SortedSet
和 SortedMap
的实现类中。示例:
public class Person implements Comparable<Person> {
private String name;
private int age;
// 构造函数、getter和setter等省略
@Override
public int compareTo(Person otherPerson) {
// 比较逻辑,根据需要定义比较的方式
return this.age - otherPerson.age;
}
}
Comparator
接口是一个外部比较器接口,它允许创建独立的比较器实现来进行对象的比较。Comparator
接口不会影响类的实现,可以在需要时创建多个不同的比较规则。
方法签名:
public interface Comparator<T> {
int compare(T o1, T o2);
}
用途:
Comparator
接口适用于需要在不同情况下使用不同的比较逻辑的场景。Comparator
来对类的对象进行排序,而不需要修改类本身或者使用其默认的比较方式。示例:
public class PersonAgeComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
}
Comparable vs Comparator:
Comparable
接口是对象自身的内部比较方式,类必须实现它来定义对象之间的默认比较规则。Comparator
接口是一个独立的比较器,允许定义多种不同的比较规则,并在需要时动态选择和应用这些规则。适用场景:
Comparable
接口当类的自然顺序(默认的排序方式)已经被明确定义时。Comparator
接口当需要定义额外的、非默认的比较规则,或者在不同的场景下使用不同的比较方式时。总结来说,Comparable
和 Comparator
是Java中用于对象比较和排序的两种不同方式,每种方式都有其适用的场景和优势,开发者可以根据具体需求选择合适的接口来实现对象的比较和排序功能。
Java的异常处理机制旨在提高代码的健壮性和可维护性,通过捕获和处理异常来防止程序在运行时崩溃。Java使用try
, catch
, finally
和 throw
关键字来实现异常处理。
Java中的异常主要分为两大类:
IOException
, SQLException
NullPointerException
, ArrayIndexOutOfBoundsException
try-catch:
用于捕获和处理异常。在try
块中放置可能会抛出异常的代码,在catch
块中处理该异常。
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 处理异常的代码
}
finally:
finally
块用于执行一些重要的清理代码,不论是否抛出异常,该块中的代码都会执行。
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 处理异常的代码
} finally {
// 总是执行的代码
}
throw:
用于显式抛出异常对象。
public void someMethod() throws Exception {
if (someCondition) {
throw new Exception("错误信息");
}
}
throws:
用于在方法签名中声明该方法可能抛出的异常,提醒调用者处理这些异常。
public void someMethod() throws IOException {
// 可能抛出IOException的代码
}
以下是一个简单的例子,展示了如何使用try
, catch
, finally
和throw
来处理异常:
import java.io.*;
public class ExceptionExample {
public static void main(String[] args) {
try {
// 可能抛出IOException的代码
readFile("test.txt");
} catch (IOException e) {
// 处理IOException
System.out.println("An error occurred: " + e.getMessage());
} finally {
// 总是执行的代码
System.out.println("Execution finished.");
}
}
public static void readFile(String fileName) throws IOException {
FileReader file = new FileReader(fileName);
BufferedReader fileInput = new BufferedReader(file);
// 打印文件内容
for (int counter = 0; counter < 3; counter++) {
System.out.println(fileInput.readLine());
}
fileInput.close();
}
}
Java允许开发者创建自定义异常类,通常用于特定业务逻辑中的错误处理。自定义异常需要继承Exception
或RuntimeException
。
public class CustomException extends Exception {
public CustomException(String message) {
super(message);
}
}
public class TestCustomException {
public static void main(String[] args) {
try {
validateAge(15);
} catch (CustomException e) {
System.out.println("Caught custom exception: " + e.getMessage());
}
}
static void validateAge(int age) throws CustomException {
if (age < 18) {
throw new CustomException("Age must be 18 or above.");
}
}
}
try-with-resources
try-with-resources
是Java 7引入的一种资源管理机制,用于简化资源(如文件、数据库连接等)关闭的代码。它确保了任何实现了 AutoCloseable
接口的资源在使用完后会自动关闭,从而减少资源泄漏的风险。
try-with-resources
的基本语法如下:
try (ResourceType resource = new ResourceType()) {
// 使用资源的代码
} catch (ExceptionType e) {
// 处理异常的代码
}
其中,ResourceType
必须是实现了 AutoCloseable
或 Closeable
接口的类。
当try-with-resources
语句结束时,无论是否抛出异常,都会自动调用资源的 close()
方法。这确保了资源的正确释放。
下面是一个使用 try-with-resources
读取文件的例子:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResourcesExample {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}
在这个例子中:
BufferedReader
和 FileReader
都实现了 AutoCloseable
接口。try
块结束时,不论是否发生异常,BufferedReader
和 FileReader
的 close()
方法都会被自动调用。try-with-resources
语句可以管理多个资源,多个资源之间用分号分隔:
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"));
PrintWriter pw = new PrintWriter(new FileWriter("output.txt"))) {
String line;
while ((line = br.readLine()) != null) {
pw.println(line);
}
} catch (IOException e) {
System.out.println("An error occurred: " + e.getMessage());
}
在这个例子中,BufferedReader
和 PrintWriter
都会在try
块结束时自动关闭。
任何实现了 AutoCloseable
接口的类都可以使用 try-with-resources
语句。以下是一个自定义资源类的例子:
public class CustomResource implements AutoCloseable {
public void useResource() {
System.out.println("Using resource");
}
@Override
public void close() {
System.out.println("Closing resource");
}
}
public class TryWithResourcesCustomExample {
public static void main(String[] args) {
try (CustomResource resource = new CustomResource()) {
resource.useResource();
}
}
}
在这个例子中,当try
块结束时,CustomResource
的 close()
方法会被自动调用。
try-with-resources
是Java中处理资源管理的一种简洁高效的方式。通过自动管理资源的关闭,它不仅简化了代码,还提高了程序的安全性和可靠性。
在Java中,文件读写操作主要通过java.io
包中的类来实现。以下是一些常用类和方法,分别用于读取和写入文件。
FileReader
和 BufferedReader
FileReader
类用于读取字符文件。BufferedReader
提供缓冲读取,提高了读取效率。
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class FileReadExample {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileInputStream
和 InputStreamReader
FileInputStream
类用于读取字节文件,通常与 InputStreamReader
结合使用,将字节流转换为字符流。
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
public class FileReadExample2 {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("example.txt")))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileWriter
和 BufferedWriter
FileWriter
类用于写入字符文件。BufferedWriter
提供缓冲写入,提高了写入效率。
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class FileWriteExample {
public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("example.txt"))) {
bw.write("Hello, World!");
bw.newLine();
bw.write("Java File Writing Example.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileOutputStream
FileOutputStream
类用于写入字节文件。
import java.io.FileOutputStream;
import java.io.IOException;
public class FileWriteExample2 {
public static void main(String[] args) {
try (FileOutputStream fos = new FileOutputStream("example.txt")) {
String content = "Hello, World!\nJava File Writing Example.";
fos.write(content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java NIO(New IO)提供了更加高效的文件操作方法。
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.io.IOException;
public class FileReadNIOExample {
public static void main(String[] args) {
try {
List<String> lines = Files.readAllLines(Paths.get("example.txt"));
for (String line : lines) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class FileWriteNIOExample {
public static void main(String[] args) {
String content = "Hello, World!\nJava NIO File Writing Example.";
try {
Files.write(Paths.get("example.txt"), content.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileReader
和 BufferedReader
适合读取字符文件。FileInputStream
和 InputStreamReader
适合读取字节文件,并可以将字节流转换为字符流。FileWriter
和 BufferedWriter
适合写入字符文件。FileOutputStream
适合写入字节文件。在选择使用哪个类时,可以根据具体需求和文件类型来决定。
定义:Lambda表达式是Java 8引入的一种简洁的方式,用于实现匿名函数。它可以使代码更加简洁、可读性更强,特别是在需要使用短小的代码段来实现接口方法时。
语法:
(parameters) -> expression
或
(parameters) -> { statements; }
示例:
// 使用Lambda表达式实现Runnable接口
Runnable r = () -> System.out.println("Hello, Lambda!");
r.run();
定义:Functional接口是指仅包含一个抽象方法的接口。这种接口可以隐式地转换为Lambda表达式。Java 8引入了@FunctionalInterface
注解,用于显式地声明一个接口为Functional接口,但这不是强制的,任何满足条件的接口都可以作为Functional接口。
示例:
@FunctionalInterface
interface MyFunctionalInterface {
void myMethod();
}
常见的Functional接口:
java.lang.Runnable
:只有一个run
方法。java.util.concurrent.Callable
:只有一个call
方法。java.util.function.Predicate
:只有一个test
方法,用于条件判断。java.util.function.Function
:只有一个apply
方法,用于将一个值转换为另一个值。java.util.function.Consumer
:只有一个accept
方法,用于处理一个输入值而不返回结果。java.util.function.Supplier
:只有一个get
方法,用于提供一个值。Java的Functional接口提供了许多通用的接口类型,每个接口在不同的场景中使用,用于解决特定类型的问题。以下是常见的Functional接口及其典型使用场景:
Runnable
接口使用场景:用于在新线程中执行代码块。
示例:
Runnable task = () -> System.out.println("Running in a separate thread");
new Thread(task).start();
Callable<V>
接口使用场景:与Runnable
类似,但可以返回结果或抛出异常,常用于需要返回结果的异步任务。
示例:
import java.util.concurrent.Callable;
Callable<Integer> task = () -> {
return 123;
};
Predicate<T>
接口使用场景:用于进行条件判断,返回true
或false
。常用于过滤操作。
示例:
import java.util.function.Predicate;
Predicate<String> isLongerThan5 = s -> s.length() > 5;
System.out.println(isLongerThan5.test("Hello")); // false
System.out.println(isLongerThan5.test("Hello, World!")); // true
典型应用:在集合过滤中使用
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> longNames = names.stream()
.filter(isLongerThan5)
.collect(Collectors.toList());
Function<T, R>
接口使用场景:用于将一种类型的数据转换为另一种类型,常用于映射操作。
示例:
import java.util.function.Function;
Function<Integer, String> intToString = i -> "Number: " + i;
System.out.println(intToString.apply(5)); // "Number: 5"
典型应用:在集合映射中使用
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<String> numberStrings = numbers.stream()
.map(intToString)
.collect(Collectors.toList());
Consumer<T>
接口使用场景:用于对单个输入执行某些操作,但不返回结果,常用于遍历操作或打印操作。
示例:
import java.util.function.Consumer;
Consumer<String> printUpperCase = s -> System.out.println(s.toUpperCase());
printUpperCase.accept("hello"); // "HELLO"
典型应用:在集合遍历中使用
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(printUpperCase);
Supplier<T>
接口使用场景:用于提供或生成一个结果,不接受任何输入,常用于延迟计算或对象实例化。
示例:
import java.util.function.Supplier;
Supplier<Double> randomValue = () -> Math.random();
System.out.println(randomValue.get()); // 随机数
典型应用:延迟加载或默认值提供
public class LazyInitialization {
private Supplier<HeavyObject> heavyObjectSupplier = () -> createHeavyObject();
private HeavyObject createHeavyObject() {
// 创建一个重型对象
return new HeavyObject();
}
public HeavyObject getHeavyObject() {
return heavyObjectSupplier.get();
}
}
UnaryOperator<T>
接口使用场景:用于对单个操作数进行操作,并返回与操作数相同类型的结果。它是Function
的特殊化形式。
示例:
import java.util.function.UnaryOperator;
UnaryOperator<Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
典型应用:在集合操作中使用
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNumbers = numbers.stream()
.map(square)
.collect(Collectors.toList());
BinaryOperator<T>
接口使用场景:用于对两个操作数进行操作,并返回与操作数相同类型的结果。它是BiFunction
的特殊化形式。
示例:
import java.util.function.BinaryOperator;
BinaryOperator<Integer> sum = (a, b) -> a + b;
System.out.println(sum.apply(2, 3)); // 5
典型应用:在归约操作中使用
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int total = numbers.stream()
.reduce(0, sum);
System.out.println(total); // 15
BiFunction<T, U, R>
接口使用场景:用于将两个输入类型的数据转换为另一种类型,常用于需要两个输入参数的情况。
示例:
import java.util.function.BiFunction;
BiFunction<String, String, Integer> compareLengths = (a, b) -> a.length() - b.length();
System.out.println(compareLengths.apply("hello", "world!")); // -1
Java 的 Functional 接口广泛应用于各种场景中,特别是在集合操作、并发处理、条件判断、数据转换和遍历操作中。Lambda 表达式与 Functional 接口的结合,使得代码更加简洁、易读和可维护。在实际开发中,根据需求选择适当的 Functional 接口,可以大大提高代码的效率和质量。
在Java中,Lambda表达式和方法引用(Method Reference)都是简化代码的一种方式。方法引用是Lambda表达式的一种简写形式,用于直接引用已有的方法,而不需要显式地写出Lambda表达式。
Java的Stream API是从Java 8引入的一个新特性,提供了一种高效且易于使用的方式来处理集合(如List、Set、Map等)中的数据。Stream API允许你以声明性编程的方式进行集合的操作,比如过滤、映射、归约等。
filter
、map
、flatMap
、sorted
、distinct
等。forEach
、collect
、reduce
、count
等。终端操作后Stream不再可用。Stream可以通过多种方式创建:
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
String[] array = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(array);
Stream<String> stream = Stream.of("a", "b", "c");
Stream<Double> stream = Stream.generate(Math::random).limit(10);
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1).limit(10);
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
List<String> strings = Arrays.asList("a", "b", "c");
List<String> upperStrings = strings.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
List<String> strings = Arrays.asList("d", "a", "c", "b");
List<String> sortedStrings = strings.stream()
.sorted()
.collect(Collectors.toList());
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3);
List<Integer> uniqueNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
终端操作符后,才被执行。
List<String> list = Arrays.asList("a", "b", "c");
list.stream().forEach(System.out::println);
List<String> list = Arrays.asList("a", "b", "c");
List<String> upperList = list.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
List<String> list = Arrays.asList("a", "b", "c");
long count = list.stream().count();
Stream API还支持并行处理,可以通过parallelStream()
方法或parallel()
方法将Stream转换为并行流,以利用多核处理器的优势提高性能。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.parallelStream()
.reduce(0, Integer::sum);
以下是一个综合示例,展示了如何使用Stream API对集合进行一系列操作:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Edward");
List<String> filteredAndSortedNames = names.stream()
.filter(name -> name.length() > 3)
.sorted()
.map(String::toUpperCase)
.collect(Collectors.toList());
filteredAndSortedNames.forEach(System.out::println);
以上代码首先过滤掉长度小于等于3的名字,然后对剩下的名字进行排序,将名字转换为大写,最后收集到一个新的列表中,并打印出来。
ClassName::staticMethodName
Function<Integer, String> func = String::valueOf;
instance::instanceMethodName
String str = "Hello";
Supplier<Integer> func = str::length;
ClassName::instanceMethodName
Function<String, Integer> func = String::length;
ClassName::new
Supplier<List<String>> func = ArrayList::new;
Lambda表达式与方法引用的对比:
List<String> list = Arrays.asList("a", "b", "c");
list.forEach(s -> System.out.println(s));
List<String> list = Arrays.asList("a", "b", "c");
list.forEach(System.out::println);
因为它是一种引用,所以方法必须事先存在。
在Java中,内存的管理和分配是一个重要的概念,主要包括堆(Heap)、栈(Stack)、和元空间(Metaspace)。这些不同的内存区域用于不同的目的和数据类型。下面是对它们的简单解释:
String str = new String("Hello");
,这个字符串对象会被分配在堆内存中。int
、char
等)的变量和对象引用(而非对象本身)。int a = 5;
会存储在栈中。并发(Concurrency)和多线程(Multithreading)是计算机科学中用于提高程序性能和响应能力的重要概念。尽管它们经常一起使用,但实际上代表了不同的概念。下面是对这两个术语的详细解释:
定义: 并发是指一个系统能够同时处理多个任务的能力。它并不一定意味着同时执行,而是表示系统能够管理多个任务的交替执行。
特性:
示例:
实现技术:
定义: 多线程是一种实现并发的编程技术,允许程序创建多个线程在同一进程内同时执行。
特性:
示例:
Thread
类或Runnable
接口创建和管理线程。threading
模块实现多线程。thread.join()
将等待进程。优点:
挑战:
并发适用于更广泛的应用,如网络服务器、数据库、GUI应用等,而多线程通常用于需要在同一进程中进行并发的场景。
Thread
类或实现Runnable
接口来创建线程。java.util.concurrent
包,提供了更高层次的并发工具,如线程池、并发集合和同步辅助工具。ExecutorService
接口和其实现类ThreadPoolExecutor
提供线程池管理,避免频繁创建销毁线程。ReentrantLock
、CountDownLatch
、Semaphore
等提供线程间的同步机制。ConcurrentHashMap
、CopyOnWriteArrayList
等为多线程环境提供线程安全的集合实现。在Java编程中,Atomic Classes是Java并发编程(Java Concurrency)中的一部分,属于java.util.concurrent.atomic
包。它们用于处理多线程环境下的共享变量操作,提供了对基本类型和对象引用的原子操作支持。这些类通过使用无锁(lock-free)算法来提高性能和线程安全性。
以下是Java中常用的原子类:
AtomicInteger
int
类型的变量进行原子操作。get()
: 获取当前值。set(int newValue)
: 设置新的值。incrementAndGet()
: 增加1并返回增加后的值。decrementAndGet()
: 减少1并返回减少后的值。addAndGet(int delta)
: 增加指定的值并返回增加后的值。compareAndSet(int expect, int update)
: 如果当前值等于预期值,则设置为更新值。AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.incrementAndGet(); // 增加1
atomicInteger.compareAndSet(1, 2); // 如果当前值是1,更新为2
AtomicLong
long
类型的变量进行原子操作。getAndIncrement()
: 获取当前值并递增。getAndDecrement()
: 获取当前值并递减。addAndGet(long delta)
: 增加指定的值并返回增加后的值。AtomicLong atomicLong = new AtomicLong(100L);
atomicLong.addAndGet(10L); // 增加10
AtomicBoolean
boolean
类型的变量进行原子操作。get()
: 获取当前值。set(boolean newValue)
: 设置新的值。compareAndSet(boolean expect, boolean update)
: 如果当前值等于预期值,则设置为更新值。AtomicBoolean atomicBoolean = new AtomicBoolean(false);
atomicBoolean.compareAndSet(false, true); // 如果当前值是false,更新为true
AtomicReference<V>
get()
: 获取当前引用。set(V newValue)
: 设置新的引用。compareAndSet(V expect, V update)
: 如果当前引用等于预期引用,则设置为更新引用。AtomicReference<String> atomicReference = new AtomicReference<>("initial");
atomicReference.compareAndSet("initial", "updated"); // 如果当前引用是"initial",更新为"updated"
AtomicIntegerArray
和 AtomicLongArray
int
和long
数组元素的原子操作。get(int i)
: 获取索引i处的值。set(int i, int newValue)
: 设置索引i处的新值。compareAndSet(int i, int expect, int update)
: 如果索引i处的当前值等于预期值,则设置为更新值。AtomicIntegerArray atomicArray = new AtomicIntegerArray(5);
atomicArray.set(0, 10);
atomicArray.incrementAndGet(0); // 增加索引0处的值
AtomicBoolean
可以用来实现线程间的标志位。synchronized
)性能更高。以下是一个简单的示例,展示了如何使用AtomicInteger
来实现线程安全的计数器:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet();
}
public int getValue() {
return counter.get();
}
public static void main(String[] args) throws InterruptedException {
AtomicCounter atomicCounter = new AtomicCounter();
// 创建多个线程来同时增加计数器
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicCounter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicCounter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// 输出计数器的最终值
System.out.println("Final Counter Value: " + atomicCounter.getValue());
}
}
Java中的Atomic Classes为开发者提供了一种高效、线程安全的方式来操作共享变量。这些类通过支持无锁操作来减少线程间的争用,从而提高应用程序的并发性能。在需要保证线程安全且性能要求较高的场景下,它们是非常有用的工具。
在Java编程中,synchronized关键字用于实现线程同步,以确保多个线程访问共享资源时不会出现数据不一致或竞争条件。通过在方法或代码块上使用synchronized关键字,可以保证同时只有一个线程执行同步代码,从而实现线程安全。
synchronized关键字可以用于方法和代码块。
在方法上使用synchronized关键字,可以确保同时只有一个线程可以执行该方法。
实例方法同步
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
静态方法同步
public class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
使用synchronized关键字锁定特定的对象,从而实现对代码块的同步。相较于同步方法,代码块同步可以减少同步范围,提高并发性能。
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
synchronized通过一个锁机制来实现同步。当一个线程访问synchronized代码时,它必须获得锁。其他线程如果试图访问相同的同步代码块,则必须等待,直到锁被释放。锁可以是任何对象。
Class
对象)。可重入性:一个线程可以多次获得相同的锁。即一个线程可以进入它已经拥有锁的代码块或方法。
public synchronized void outer() {
inner();
}
public synchronized void inner() {
// 因为可重入性,线程可以进入此方法
}
阻塞性:如果一个线程持有锁,其他线程必须等待锁被释放后才能继续。
可能导致死锁:如果不小心,多个线程可能会相互等待,导致死锁。
在Java中,Concurrent Collections是java.util.concurrent
包的一部分,为多线程环境设计的集合类。它们提供了一种更高效的方式来处理并发访问,避免了传统集合类在多线程环境中可能出现的同步问题。相比于使用synchronized
关键字进行显式同步,Concurrent Collections提供了更细粒度的锁和无锁机制,从而提高性能和可扩展性。
在多线程编程中,如果多个线程同时访问共享的集合对象,可能会导致以下问题:
synchronized
进行显式同步,可能导致线程阻塞和性能瓶颈。Concurrent Collections通过提供线程安全的集合实现,解决了这些问题。
以下是Java中常用的并发集合:
Hashtable
或同步包装的HashMap
,它使用了分段锁(segment locking)机制来提高并发性能。computeIfAbsent
、compute
、merge
等。put
、get
、remove
等基本操作。示例代码:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.computeIfAbsent("C", k -> 3); // 如果"C"不存在,插入key和计算的value
map.forEach((key, value) -> System.out.println(key + ": " + value));
}
}
ArrayList
实现,使用写时复制(Copy-On-Write)机制。add
、set
)会创建底层数组的新副本,保证读取时的线程安全。示例代码:
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.forEach(System.out::println); // 遍历列表
}
}
Set
实现,基于CopyOnWriteArrayList
。示例代码:
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class CopyOnWriteArraySetExample {
public static void main(String[] args) {
Set<String> set = new CopyOnWriteArraySet<>();
set.add("A");
set.add("B");
set.forEach(System.out::println); // 遍历集合
}
}
示例代码:
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ConcurrentLinkedQueueExample {
public static void main(String[] args) {
Queue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("A");
queue.offer("B");
String head = queue.poll(); // 移除并返回队列头
System.out.println("Head: " + head);
}
}
ArrayBlockingQueue
: 有界阻塞队列,基于数组实现。LinkedBlockingQueue
: 可选有界阻塞队列,基于链表实现。PriorityBlockingQueue
: 支持优先级排序的无界阻塞队列。DelayQueue
: 支持延迟元素的无界阻塞队列。示例代码(ArrayBlockingQueue):
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
queue.put("A"); // 将元素放入队列,若队列满则等待
String head = queue.take(); // 获取并移除队列头,若队列空则等待
System.out.println("Head: " + head);
}
}
示例代码(ConcurrentSkipListMap):
import java.util.concurrent.ConcurrentSkipListMap;
public class ConcurrentSkipListMapExample {
public static void main(String[] args) {
ConcurrentSkipListMap<String, Integer> map = new ConcurrentSkipListMap<>();
map.put("A", 1);
map.put("B", 2);
map.forEach((key, value) -> System.out.println(key + ": " + value));
}
}
synchronized
进行显式同步,Concurrent Collections提供了更高的性能和可扩展性。Java中的ExecutorService
和线程池是并发编程中非常重要的工具,它们提供了一种有效管理和执行多线程任务的方法。通过使用这些工具,开发者可以更轻松地控制线程的创建和执行,从而提高应用程序的性能和可维护性。
ExecutorService
是Java提供的一个接口,位于java.util.concurrent
包中。它是一个更高级的线程管理机制,相较于手动创建和管理线程,ExecutorService
提供了以下优势:
submit(Runnable task)
: 提交一个实现Runnable
接口的任务进行执行。submit(Callable<T> task)
: 提交一个实现Callable
接口的任务进行执行,并返回一个Future
对象。invokeAll(Collection<? extends Callable<T>> tasks)
: 执行批量任务,等待所有任务完成。invokeAny(Collection<? extends Callable<T>> tasks)
: 执行批量任务,返回第一个完成任务的结果。shutdown()
: 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。shutdownNow()
: 尝试停止所有正在执行的任务,并返回等待执行的任务列表。以下是一个简单的ExecutorService
使用示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceExample {
public static void main(String[] args) {
// 创建一个固定线程数的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交Runnable任务
executorService.submit(() -> {
System.out.println("Runnable Task 1 executed by: " + Thread.currentThread().getName());
});
executorService.submit(() -> {
System.out.println("Runnable Task 2 executed by: " + Thread.currentThread().getName());
});
// 提交Callable任务并获取结果
executorService.submit(() -> {
System.out.println("Callable Task executed by: " + Thread.currentThread().getName());
return "Result from Callable Task";
});
// 关闭线程池
executorService.shutdown();
}
}
线程池是Java中管理多个线程的一种机制,旨在通过减少线程创建和销毁的开销来提高性能。线程池可以重复使用已经创建的线程来执行多个任务,从而避免频繁的线程创建和销毁所带来的性能损耗。
Java通过Executors
工厂类提供了多种常用的线程池实现:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
以下是一个使用FixedThreadPool
的示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小为3的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交多个任务
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.submit(() -> {
System.out.println("Task " + index + " executed by: " + Thread.currentThread().getName());
});
}
// 关闭线程池
executorService.shutdown();
}
}
在Java 8中,可以通过ThreadPoolExecutor
类自定义线程池配置:
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 创建自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize
4, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS, // keepAliveTime单位
new ArrayBlockingQueue<>(10) // 工作队列
);
// 提交任务
for (int i = 0; i < 10; i++) {
final int index = i;
executor.submit(() -> {
System.out.println("Custom Task " + index + " executed by: " + Thread.currentThread().getName());
});
}
// 关闭线程池
executor.shutdown();
}
}
Java线程池使用不同的排队策略来处理任务:
Java中的ExecutorService
和线程池提供了强大的并发任务管理功能,帮助开发者更高效地执行多线程任务。通过使用这些工具,可以减少线程管理的复杂性,提高应用程序的性能和可维护性。在实际应用中,合理选择和配置线程池将有助于实现高效的并发编程。