Java基础知识
本文最后更新于 2023-12-09,文章内容可能已经过时。
Java基础知识.
做八股和日常开发中查缺补漏之用.
1.抽象类和接口的区别:
抽象类(Abstract Class)和接口(Interface)是面向对象编程中两个不同的概念,它们有一些关键的区别:
1. 定义:
抽象类: 一个抽象类可以包含抽象方法和具体方法。抽象方法是没有具体实现的方法,而具体方法有具体的实现。抽象类不能被实例化,只能被继承。
javaCopy codeabstract class AbstractClass { abstract void abstractMethod(); void concreteMethod() { // 具体实现 } }
接口: 接口中只能包含抽象方法和常量(在Java 8之前,接口只能包含抽象方法,Java 8引入了默认方法和静态方法)。接口定义了一种规范,任何实现该接口的类都需要提供具体的实现。
javaCopy codeinterface MyInterface { void abstractMethod(); // 默认方法 default void defaultMethod() { // 具体实现 } }
2. 多继承:
抽象类: 一个类只能继承一个抽象类,因为Java是单继承的。继承抽象类可以继承具体方法的实现。
接口: 一个类可以实现多个接口,从而达到多继承的效果。接口中只包含抽象方法,实现类需要提供具体的实现。
3. 构造器:
抽象类: 抽象类可以有构造器,并且构造器可以用于初始化抽象类的成员。
接口: 接口不能有构造器,因为接口中的成员都是隐式的 public static final,而构造器是用于创建对象的,不符合接口的特性。
4. 成员变量:
抽象类: 可以包含实例变量、静态变量,也可以包含普通方法。
接口: 只能包含 public static final 的常量,不包含实例变量和普通方法(在Java 8之前)。
5. 实现方式:
抽象类: 使用关键字
abstract
定义。接口: 使用关键字
interface
定义。
6. 使用场景:
抽象类: 适合用于某些类之间有共同的实现,并且希望子类能够继承这些实现的情况。
接口: 适合用于定义一些规范、约定,让多个类去实现这些规范,实现多态性。
2. 线程池:
Java线程池是一种用于管理和复用线程的机制,它提供了一种优雅的方式来处理多线程任务。线程池的主要目标是降低线程的创建和销毁的开销,通过重用线程来提高程序的性能和稳定性。Java线程池是通过java.util.concurrent
包中的Executor
框架来实现的。
1. 核心组件:
Executor(执行器): 是线程池的顶层接口,负责执行提交的任务。
ExecutorService(执行器服务): 是
Executor
的子接口,提供了更丰富的任务提交方式,以及管理线程池的方法。ThreadPoolExecutor(线程池执行器): 是
ExecutorService
的实现类,是Java线程池的核心实现。它负责管理线程的创建、执行、关闭等。
2. 创建线程池:
可以使用Executors
工厂类来创建不同类型的线程池:
javaCopy codeExecutorService executor = Executors.newFixedThreadPool(5); // 创建固定大小的线程池
ExecutorService executor = Executors.newCachedThreadPool(); // 创建可缓存的线程池
ExecutorService executor = Executors.newSingleThreadExecutor(); // 创建单线程的线程池
ExecutorService executor = Executors.newScheduledThreadPool(5); // 创建定时执行的线程池
3. 提交任务:
使用submit
方法提交任务给线程池:
javaCopy codeexecutor.submit(() -> {
// 任务的具体逻辑
});
4. 关闭线程池:
使用shutdown
方法关闭线程池。调用shutdown
后,线程池不再接受新的任务,但会等待已经提交的任务执行完成。
javaCopy code
executor.shutdown();
5. 线程池的参数:
corePoolSize(核心线程数): 线程池中始终保持存活的线程数,即使它们处于空闲状态。
maximumPoolSize(最大线程数): 线程池中允许的最大线程数。
keepAliveTime(线程空闲时间): 当线程池中的线程数量大于核心线程数时,多余的空闲线程在被终止之前等待新任务的最长时间。
workQueue(工作队列): 用于保存等待执行的任务的阻塞队列。
ThreadFactory(线程工厂): 用于创建新线程的工厂。
6. 常用线程池类型:
FixedThreadPool(固定大小线程池): 有固定数量的核心线程,没有非核心线程,任务队列使用无界队列。
CachedThreadPool(可缓存线程池): 只有非核心线程,其数量不固定,可以根据需求自动创建新线程,空闲线程会在一定时间内回收。
SingleThreadExecutor(单线程线程池): 只有一个核心线程,确保所有任务按照指定顺序执行。
ScheduledThreadPool(定时线程池): 用于执行定时任务和周期性任务。
7. 优点:
降低资源消耗: 重复利用线程,减少线程的创建和销毁的开销。
提高响应速度: 可以更快速地响应任务的到达,因为线程不需要重新创建。
统一管理: 线程池提供了对线程的统一管理和监控。
8. 注意事项:
需要根据具体业务场景和性能要求选择合适的线程池类型和参数。
要确保提交的任务能够适应线程池的工作方式,避免因为任务执行时间过长或者阻塞而影响整个线程池的性能。
使用完线程池后,及时关闭,防止资源泄漏。
Java线程池是多线程编程中的重要工具,合理使用线程池可以提高系统的性能、稳定性,并减少对系统资源的占用。
3.同步锁:
在Java中,同步锁是一种用于控制多线程并发访问共享资源的机制。通过同步锁,可以确保在同一时刻只有一个线程能够访问共享资源,从而避免多个线程对共享数据的并发修改导致的数据不一致或者其他并发问题。Java提供了多种同步锁机制,其中最常见的包括synchronized关键字、ReentrantLock类以及相关的Lock接口。
1. synchronized关键字:
synchronized
关键字可以用来修饰方法或代码块,它提供了一种隐式的锁机制,使得同一时刻只有一个线程能够进入被synchronized
修饰的方法或代码块。
使用方法:
修饰方法:
javaCopy codepublic synchronized void synchronizedMethod() { // 方法体 }
修饰代码块:
javaCopy codepublic void someMethod() { // 非同步代码 synchronized (lockObject) { // 需要同步的代码块 } // 非同步代码 }
2. ReentrantLock类:
ReentrantLock
是java.util.concurrent.locks
包下的一个锁实现类。与synchronized
相比,ReentrantLock
提供了更灵活的锁操作,可以支持可中断锁、定时锁等特性。
使用方法:
javaCopy codeimport java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private final Lock lock = new ReentrantLock();
public void someMethod() {
lock.lock(); // 获取锁
try {
// 需要同步的代码块
} finally {
lock.unlock(); // 释放锁
}
}
}
3. Lock接口的其他实现类:
除了ReentrantLock
外,Java还提供了其他一些锁的实现类,例如ReentrantReadWriteLock
、StampedLock
等,它们针对不同的场景提供了更灵活的锁机制。
4. 同步锁的注意事项:
避免死锁: 当多个线程同时持有一组锁,并且每个线程都在等待另一个线程释放锁时,可能发生死锁。因此,使用同步锁时要特别小心,避免出现死锁情况。
避免饥饿: 使用同步锁时,要考虑到线程饥饿的问题。饥饿指的是一个线程由于始终无法获取所需的锁而无法执行。为了避免饥饿,可以使用公平锁或者适当设计锁的获取顺序。
使用tryLock避免死锁: 在使用
ReentrantLock
时,可以使用tryLock
方法来尝试获取锁,避免线程在等待锁时发生死锁。
同步锁是多线程编程中重要的工具,通过合理地使用同步锁,可以确保线程安全,避免竞态条件和数据不一致等问题。在选择使用synchronized
关键字还是ReentrantLock
时,要根据具体的需求和场景做出合适的选择。
4.字符串转换:
在Java中,可以通过字符串的toCharArray
方法将字符串转换成字符数组,然后遍历该字符数组来访问每个字符。另外,也可以直接使用字符串的charAt
方法获取指定位置的字符。
以下是两种常见的方法:
1. 使用 toCharArray
方法:
javaCopy codeString str = "Hello";
// 将字符串转换为字符数组
char[] charArray = str.toCharArray();
// 遍历字符数组
for (char c : charArray) {
System.out.println(c);
}
2. 使用 charAt
方法:
javaCopy codeString str = "Hello";
// 获取字符串的长度
int length = str.length();
// 遍历字符串,通过 charAt 获取每个字符
for (int i = 0; i < length; i++) {
char c = str.charAt(i);
System.out.println(c);
}
这两种方法都可以用于遍历字符串中的每个字符。选择使用哪种方法取决于具体的需求和场景。 toCharArray
可能更适合需要对字符进行修改或者其他数组操作的情况,而 charAt
则更简单直观。
5.参数传递:
在Java中,方法参数的传递方式有两种:按值传递(pass by value)和按引用传递(pass by reference)。
按值传递(Pass by Value):
对于基本数据类型(如int、float、char等),实际上是将变量的值传递给了方法,而不是变量本身。在方法内对参数的修改不会影响到原始值。
javaCopy codepublic class PassByValueExample { public static void main(String[] args) { int num = 10; System.out.println("Before: " + num); modifyValue(num); System.out.println("After: " + num); } public static void modifyValue(int x) { x = 20; } }
输出结果:
makefileCopy codeBefore: 10 After: 10
按引用传递(Pass by Reference):
对于对象引用,实际上是将引用的副本传递给了方法,而不是对象本身。方法内对引用所指向的对象的修改会影响原始对象。
javaCopy codepublic class PassByReferenceExample { public static void main(String[] args) { StringBuilder str = new StringBuilder("Hello"); System.out.println("Before: " + str); modifyReference(str); System.out.println("After: " + str); } public static void modifyReference(StringBuilder s) { s.append(" World"); } }
输出结果:
makefileCopy codeBefore: Hello After: Hello World
尽管对象引用是按值传递的,但因为引用指向的是对象在堆中的内存地址,所以通过引用可以修改对象的状态。需要注意的是,对于基本数据类型,无法通过方法修改原始值;而对于对象引用,虽然不能改变引用本身,但可以通过引用修改对象的内容。
但是注意,有例外:Java中的Integer
是一个包装类,属于引用类型。然而,与基本数据类型一样,Integer
对象的传递也是按值传递的。这意味着将一个Integer
对象传递给方法时,实际上传递的是对象引用的副本,而不是对象本身。
看一个例子:
javaCopy codepublic class PassByValueWithInteger {
public static void main(String[] args) {
Integer num = 10;
System.out.println("Before: " + num);
modifyInteger(num);
System.out.println("After: " + num);
}
public static void modifyInteger(Integer x) {
x = 20;
}
}
输出结果:
makefileCopy codeBefore: 10
After: 10
在上述例子中,modifyInteger
方法试图修改传递进来的 Integer
对象的值,但在方法外部,原始的 num
对象并没有被修改,因为Integer
是不可变(immutable)的,任何修改都会导致创建一个新的对象。
需要注意的是,对于可变对象而言,即使是按值传递,通过引用修改对象内部的状态会影响原始对象。例如,如果使用StringBuilder
而不是Integer
,则会看到不同的结果。
Integer
对象之所以在表现上有时候看起来特殊,是因为它是一个不可变类(immutable class)。不可变类是指类的实例一旦创建后,就不能再被修改。在Java中,所有的基本包装类,如Integer
、Double
等,都是不可变的。
因为Integer
是不可变的,所以一旦创建了一个Integer
对象,就无法再修改其存储的值。任何对Integer
对象的操作实际上都会创建一个新的对象。
考虑下面的例子:
javaCopy codepublic class ImmutableExample {
public static void main(String[] args) {
Integer num = 10;
System.out.println("Before: " + num.hashCode());
num = num + 5; // 这实际上创建了一个新的 Integer 对象
System.out.println("After: " + num.hashCode());
}
}
在这个例子中,对 num
执行 + 5
操作实际上是创建了一个新的 Integer
对象,而不是在原始对象上进行修改。由于不可变性,Integer
的值在创建后就不能被修改,这与按值传递的语义相一致。
对于其他可变对象,比如StringBuilder
或自定义的可变类,按值传递时修改对象内部的状态会影响原始对象。因此,Integer
的特殊性主要来自于它的不可变性。
在Java中,除了Integer
之外,还有许多其他的基本数据类型的包装类,它们也是不可变的。这些包装类包括:
Byte:
java.lang.Byte
Short:
java.lang.Short
Long:
java.lang.Long
Float:
java.lang.Float
Double:
java.lang.Double
Character:
java.lang.Character
Boolean:
java.lang.Boolean
所有这些类都是不可变的,这意味着一旦创建了对象,就不能修改其内容。每次对这些对象进行修改操作(例如在Integer
上执行+
运算)都会创建一个新的对象。
除了基本数据类型的包装类之外,还有一些其他的不可变类,例如:
String:
java.lang.String
- 字符串类也是不可变的,一旦创建了字符串,就不能修改其内容。任何对字符串的操作都会返回一个新的字符串对象。BigInteger:
java.math.BigInteger
- 用于表示任意精度整数的不可变类。BigDecimal:
java.math.BigDecimal
- 用于表示任意精度浮点数的不可变类。
这些不可变类的设计有助于确保对象的安全性和线程安全性。不可变对象可以更容易地在多线程环境中使用,因为它们不会发生并发修改的问题。
6.Lambda表达式与函数式编程:
函数式编程(Functional Programming,简称FP)是一种编程范式,它将计算视为数学函数的求值过程,避免使用可变状态和可变数据。函数式编程强调函数的纯粹性(Pure Functions)和不可变性(Immutable Data),这意味着函数的输出仅依赖于输入,而且数据一旦创建就不能再被修改。
以下是函数式编程的一些关键特征:
纯函数(Pure Functions): 纯函数是指在相同的输入条件下,总是返回相同的输出,并且没有副作用。副作用是指函数执行过程中对程序状态的修改,如修改全局变量、文件操作等。
不可变性(Immutable Data): 函数式编程强调使用不可变数据结构,即一旦数据被创建,就不能再被修改。这有助于避免共享状态和简化程序的理解和调试。
高阶函数(Higher-Order Functions): 函数式编程支持使用高阶函数,即能够接受一个或多个函数作为参数,并且能够返回一个函数作为结果。
递归(Recursion): 函数式编程通常使用递归而非循环来实现迭代过程。
不可变性(Pattern Matching): 一些函数式编程语言支持模式匹配,这是一种结构化的方式来检查一个值是否符合某种模式。
惰性求值(Lazy Evaluation): 函数式编程中的一些语言支持延迟计算,只在需要时才进行计算。
函数组合(Function Composition): 将多个函数组合成一个新的函数,通过将一个函数的输出作为另一个函数的输入来创建更复杂的功能。
函数式编程通常与命令式编程相对立,命令式编程更关注程序的状态和对状态的改变。常见的函数式编程语言包括Haskell、Clojure、Scala,而一些主流编程语言如JavaScript、Python也提供了一些函数式编程的特性。函数式编程的优势包括代码的简洁性、可读性和更容易进行并发编程。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class FunctionalProgrammingExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva");
// 使用Lambda表达式和函数式接口实现筛选和转换
List<String> filteredNames = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Filtered and Uppercased Names: " + filteredNames);
}
}
这是一个Lambda表达式,它是Java 8引入的一种语法糖,用于表示函数式接口的实例。Lambda表达式通常用于简化匿名内部类的语法,使代码更为紧凑和可读。
在你提到的例子中,name -> name.length() > 3
表达的是一个函数,这个函数接受一个字符串参数 name
,然后返回一个布尔值。具体来说,这个Lambda表达式检查字符串的长度是否大于3。这个Lambda表达式可以被理解为一个函数,其函数体为 name.length() > 3
。
在函数式编程中,这种形式的Lambda表达式通常用于传递给高阶函数,例如在Stream API中的 filter
方法。在这个例子中,filter
方法会接受一个谓词(Predicate),即一个返回布尔值的函数,用于决定哪些元素应该被保留。
简而言之,name -> name.length() > 3
表示一个函数,接受一个字符串参数 name
,并返回一个布尔值,用于检查字符串长度是否大于3。
Lambda表达式是Java 8引入的一项重要特性,它提供了一种更简洁、更便利的方式来编写匿名函数。Lambda表达式可以用来替代Java中某些接口的匿名内部类,使代码更加紧凑和易读。
Lambda表达式的基本语法如下:
javaCopy code
(parameters) -> expression
或者是
javaCopy code
(parameters) -> { statements; }
其中,parameters
是参数列表,expression
是一个表达式,或者是一组带有花括号的语句块。
下面是一个简单的例子,比较了使用匿名内部类和Lambda表达式来实现一个接口:
使用匿名内部类:
javaCopy codeinterface MyInterface {
void myMethod(String s);
}
public class MyClass {
public static void main(String[] args) {
MyInterface myInterface = new MyInterface() {
@Override
public void myMethod(String s) {
System.out.println("Hello, " + s);
}
};
myInterface.myMethod("World");
}
}
使用Lambda表达式:
javaCopy codeinterface MyInterface {
void myMethod(String s);
}
public class MyClass {
public static void main(String[] args) {
MyInterface myInterface = (s) -> System.out.println("Hello, " + s);
myInterface.myMethod("World");
}
}
Lambda表达式的特点包括:
简洁性: 通过Lambda表达式可以更简洁地表达功能,省略了很多样板代码。
函数式编程: Lambda表达式支持函数式编程风格,使得Java更加接近函数式语言。
便利性: 可以方便地作为参数传递给方法,使代码更加灵活。
支持闭包: Lambda表达式可以访问其外部作用域的变量,形成闭包。
Lambda表达式在集合框架、并发编程、事件处理等方面有广泛的应用,极大地提升了Java语言的表达力和编程效率。