Java 常见面试题(三)
适合人群 :正在准备 Java 后端面试的求职者
技术栈 :Java 21
涵盖方向 :集合框架进阶、并发工具 AQS、Stream API 与 Lambda、设计模式、注解原理
阅读建议:每道题按「面试官问法 → 求职者回答 → 核心知识点」三段式展开,建议先自己回答再对照
一、ArrayList 和 LinkedList 的底层区别是什么?什么场景选哪个?
面试官会怎么问
"你平时用 ArrayList 多还是 LinkedList 多?它们底层有什么区别?分别在什么场景下更合适?"
追问:"ArrayList 扩容的代价有多大?LinkedList 的 Node 节点在内存里是怎么分布的?"
求职者该怎么答
底层数据结构完全不同:
| 对比维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组(Object\[\]) | 双向链表(Node 节点链) |
| 随机访问 | O(1),支持 get(i) 直接定位 |
O(n),必须从头遍历 |
| 头部插入/删除 | O(n),需搬移元素 | O(1),只改指针 |
| 尾部插入 | 均摊 O(1),偶尔触发扩容 | O(1) |
| 内存占用 | 连续内存,CPU 缓存友好 | 离散内存,每个节点额外存 prev/next 指针 |
| 实现接口 | List、RandomAccess | List、Deque |
ArrayList 扩容机制:
java
// JDK 源码(简化)
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
- 每次扩容需要 新建数组 + 复制全部元素,代价 O(n)
- 默认初始容量 10,之后每次扩大 1.5 倍
- 如果能预估数据量,建议用
new ArrayList<>(expectedSize)避免频繁扩容
LinkedList 的内存布局:
java
// JDK 源码(简化)
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.prev = prev;
this.item = element;
this.next = next;
}
}
- 每个元素都是一个独立对象,堆上分散分配
- CPU 缓存预取命中率低,遍历速度实际上慢于 ArrayList
- 实现了 Deque 接口,可以当栈和队列使用
实战选择建议:
场景:频繁随机读取、尾部追加 → ArrayList(绝大多数场景)
场景:频繁头部插入/删除、当队列用 → LinkedList(或 ArrayDeque 更优)
场景:已知数据量很大 → ArrayList + 预设容量
面试加分 :实际开发中 LinkedList 用得极少,因为它既不快(内存不连续)也不省(额外指针),大多数场景 ArrayList + 预设容量就够了。如果确实需要队列,
ArrayDeque性能比 LinkedList 好得多。
核心知识点总结
- 数据结构选型:动态数组 vs 双向链表的本质区别
- CPU 缓存友好性:连续内存 vs 离散内存在现代硬件上的性能差距
- 扩容策略:1.5 倍扩容的数学原理(等比数列求和 ≈ 2 倍初始容量)
二、AQS 是什么?CountDownLatch 和 Semaphore 是怎么基于 AQS 实现的?
面试官会怎么问
"你了解 AQS 吗?它解决了什么问题?"
追问:"CountDownLatch 和 Semaphore 底层是怎么用 AQS 的?它们的区别是什么?"
再追问:"AQS 的公平模式和非公平模式有什么区别?"
求职者该怎么答
AQS(AbstractQueuedSynchronizer)是 Java 并发包的基石:
AQS 核心思想:
┌──────────────────────────────────────────────┐
│ volatile int state ← 共享资源状态 │
│ CLH 变体双向队列 ← 等待线程排队 │
│ acquire / release ← 获取与释放的模板方法 │
│ park / unpark ← 线程阻塞与唤醒 │
└──────────────────────────────────────────────┘
基于 AQS 实现的重要类:
- ReentrantLock(独占锁,state = 重入次数)
- CountDownLatch(计数器,state = 剩余计数)
- Semaphore(信号量,state = 可用许可数)
- ReentrantReadWriteLock(读写锁,state 高16位读、低16位写)
AQS 工作原理:
java
// 伪代码:AQS 获取锁的核心流程
public final void acquire(int arg) {
// 1. 先让子类尝试获取(tryAcquire)
if (!tryAcquire(arg)) {
// 2. 失败则入队
Node node = addWaiter(Node.EXCLUSIVE);
// 3. 自旋 + 阻塞等待
acquireQueued(node, arg);
}
}
// 子类实现 tryAcquire ------ 这是 AQS 的模板方法模式
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
CountDownLatch ------ 等待多个线程完成:
java
// 使用场景:主线程等待 N 个子任务全部完成
CountDownLatch latch = new CountDownLatch(3);
// 子线程完成任务后调用
latch.countDown(); // state - 1
// 主线程等待
latch.await(); // state == 0 时才放行
// 底层原理:
// - state 初始化为 N
// - countDown() → state 减 1(CAS 操作)
// - await() → 如果 state > 0,当前线程入队阻塞
// - state 减到 0 时,唤醒所有等待线程
Semaphore ------ 控制并发数量:
java
// 使用场景:限制同时访问某资源的线程数(如数据库连接池限流)
Semaphore semaphore = new Semaphore(5); // 5 个许可
// 线程获取许可
semaphore.acquire(); // state - 1,如果 state == 0 则阻塞
// 线程释放许可
semaphore.release(); // state + 1,唤醒队列中一个线程
// 底层原理:
// - state 表示可用许可数
// - acquire() → CAS 将 state 减 1,如果 < 0 则入队
// - release() → CAS 将 state 加 1,唤醒队列中等待最久的线程
CountDownLatch vs Semaphore 核心区别:
| 维度 | CountDownLatch | Semaphore |
|---|---|---|
| 用途 | 等待 N 个事件发生 | 控制并发访问数量 |
| state 含义 | 剩余计数(递减到 0) | 可用许可(可增可减) |
| 是否可重用 | 不可重用(一次性) | 可重用(acquire/release 循环) |
| 谁等待 | 调用 await 的线程 | 调用 acquire 的线程 |
| 典型场景 | 启动多个服务后开始处理 | 限流、资源池 |
公平 vs 非公平模式:
java
// 非公平(默认):新来的线程可以先尝试 CAS 抢锁,抢不到再排队
// 优点:减少上下文切换,吞吐量高
// 缺点:可能导致某些线程一直抢不到(饥饿)
// 公平:严格按队列顺序获取
// 优点:不会饥饿
// 缺点:每次都要入队出队,性能较低
Semaphore sem = new Semaphore(5, true); // 公平模式
ReentrantLock lock = new ReentrantLock(true); // 公平模式
核心知识点总结
- AQS 模板方法模式:父类提供排队/阻塞/唤醒框架,子类只实现 tryAcquire/tryRelease
- state + CLH 队列:AQS 的两大核心,state 代表资源状态,队列管理等待线程
- CAS 保证原子性:state 的修改全部通过 CAS 完成,保证线程安全
三、Java Stream API 和 Lambda 表达式,你在实际项目中怎么用的?
面试官会怎么问
"Java 8 的 Stream API 你用过吗?说说它的核心操作和原理"
追问:"Lambda 表达式和匿名内部类有什么本质区别?"
再追问:"Stream 的 parallelStream 是线程安全的吗?什么情况下用并行流?"
求职者该怎么答
Lambda 表达式的本质:
java
// Lambda 是函数式接口的语法糖,编译后生成 invokedynamic 指令
// 不会每次创建新对象(除非捕获了外部变量)
// Lambda 写法
list.sort((a, b) -> a.compareTo(b));
// 等价于匿名内部类(但底层完全不同)
list.sort(new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a.compareTo(b);
}
});
// 核心区别:
// 1. Lambda 编译为 invokedynamic,由 LambdaMetafactory 在运行时动态生成
// 2. 匿名内部类编译为独立的 .class 文件,每次 new 都创建新实例
// 3. Lambda 中 this 指向外部类,匿名内部类中 this 指向自己
Stream API 核心操作分类:
Stream 操作管道:
数据源 → [中间操作] → [中间操作] → ... → 终端操作
中间操作(惰性求值,不执行):
- filter(Predicate) 过滤
- map(Function) 映射转换
- flatMap(Function) 扁平化映射
- sorted(Comparator) 排序
- distinct() 去重
- limit(n) 截取
- skip(n) 跳过
终端操作(触发执行):
- collect(Collector) 收集结果
- forEach(Consumer) 遍历
- reduce(BinaryOp) 归约
- count() / min() / max() 聚合
- anyMatch / allMatch 匹配
- findFirst() 查找
实际项目中的典型用法:
java
// 1. 过滤 + 映射 + 收集
List<String> activeUserNames = users.stream()
.filter(User::isActive) // 过滤活跃用户
.map(User::getName) // 提取名字
.collect(Collectors.toList()); // 收集为 List
// 2. 分组统计
Map<String, Long> deptCountMap = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
));
// 3. 多字段排序
List<Employee> sorted = employees.stream()
.sorted(
Comparator.comparing(Employee::getDepartment)
.thenComparing(Employee::getSalary)
.reversed()
)
.collect(Collectors.toList());
// 4. 扁平化(嵌套集合展平)
List<String> allTags = articles.stream()
.flatMap(article -> article.getTags().stream())
.distinct()
.collect(Collectors.toList());
// 5. 归约求和
BigDecimal totalSalary = employees.stream()
.map(Employee::getSalary)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 6. partitioningBy 分流
Map<Boolean, List<Employee>> partitioned = employees.stream()
.collect(Collectors.partitioningBy(
e -> e.getSalary().compareTo(new BigDecimal("10000")) >= 0
));
// partitioned.get(true) → 薪资 >= 10000
// partitioned.get(false) → 薪资 < 10000
并行流 parallelStream 注意事项:
java
// 并行流底层使用 ForkJoinPool.commonPool()
List<Integer> result = list.parallelStream()
.filter(x -> x > 100)
.map(x -> x * 2)
.collect(Collectors.toList());
// ⚠️ 注意:
// 1. 不是线程安全的!如果操作有副作用(如修改共享变量),会出问题
List<Integer> unsafe = new ArrayList<>();
list.parallelStream().forEach(unsafe::add); // 可能丢失数据!
// 正确做法:用线程安全集合或收集到不可变结果
// 2. 数据量小(< 1000)不要用并行流,线程切换开销 > 收益
// 3. 有状态操作(sorted、distinct)在并行流中代价更高
// 4. 如果涉及 IO 操作,不要用并行流(会阻塞公共线程池)
Stream 的执行原理:
java
// Stream 本质是一个迭代器模式的链式调用
// 每次中间操作返回新的 Stream,终端操作触发遍历
// 简化模型:
stream.filter(pred).map(func).collect(list);
// 等价于一次遍历中完成所有操作(短路优化):
for (T item : source) {
if (pred.test(item)) { // filter
R result = func.apply(item); // map
list.add(result); // collect
}
// 遇到 limit/short-circuit 会提前退出
}
核心知识点总结
- Lambda 底层原理:invokedynamic 指令 + LambdaMetafactory,不是匿名内部类
- Stream 惰性求值:中间操作不执行,终端操作触发,一次遍历完成管道
- 并行流陷阱:线程安全、数据量阈值、IO 阻塞、公共线程池污染
四、面试必考的设计模式:单例、工厂、观察者,你能手写吗?
面试官会怎么问
"你能手写一个单例模式吗?为什么要那样写?"
追问:"工厂模式和抽象工厂模式有什么区别?"
再追问:"观察者模式在实际项目中用过吗?Spring 里有哪些观察者模式的体现?"
求职者该怎么答
单例模式的多种写法:
java
// 1. 饿汉式(线程安全,类加载时创建)
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
// 2. 懒汉式 + synchronized(简单但性能差)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 3. 双重检查锁 DCL(推荐)
public class Singleton {
private static volatile Singleton instance; // ⚠️ 必须 volatile
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,避免不必要的同步
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查,防止重复创建
instance = new Singleton();
}
}
}
return instance;
}
}
// 为什么必须 volatile?
// new Singleton() 不是原子操作:
// 1. 分配内存空间
// 2. 初始化对象
// 3. 将引用指向内存地址
// 指令重排序可能变成 1→3→2,另一个线程拿到未初始化的对象
// 4. 静态内部类(最优雅,推荐)
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 第一次调用时才触发 Holder 类加载
}
}
// 利用类加载机制保证线程安全,懒加载,无同步开销
// 5. 枚举(最安全,防反射 + 防序列化破坏)
public enum Singleton {
INSTANCE;
// 业务方法
public void doSomething() { }
}
单例被破坏的场景与防护:
| 破坏方式 | 防护手段 |
|---|---|
| 反射调用私有构造器 | 构造器中抛异常或检查是否已创建 |
| 序列化反序列化 | 实现 readResolve() 方法 |
| Clone | 实现 Cloneable 并抛 CloneNotSupportedException |
| 类加载器不同 | 使用统一类加载器(或枚举) |
枚举天然防反射、防序列化,是 Effective Java 推荐的最佳写法
工厂模式:
java
// 简单工厂(不属于 GoF 23 种)
public class AnimalFactory {
public static Animal create(String type) {
return switch (type) {
case "lion" -> new Lion();
case "tiger" -> new Tiger();
case "elephant" -> new Elephant();
default -> throw new IllegalArgumentException("Unknown: " + type);
};
}
}
// 工厂方法模式(每种产品对应一个工厂)
public interface AnimalFactory {
Animal create();
}
public class LionFactory implements AnimalFactory {
@Override
public Animal create() { return new Lion(); }
}
public class TigerFactory implements AnimalFactory {
@Override
public Animal create() { return new Tiger(); }
}
// 优点:符合开闭原则,新增产品只需新增工厂
// 缺点:类数量膨胀
// 抽象工厂模式(创建产品族)
public interface ZooFactory {
Animal createAnimal();
Cage createCage();
Food createFood();
}
public class SafariZooFactory implements ZooFactory {
@Override public Animal createAnimal() { return new Lion(); }
@Override public Cage createCage() { return new OpenCage(); }
@Override public Food createFood() { return new MeatFood(); }
}
// 优点:保证产品族一致性
// 缺点:新增产品等级(如新增 Toy)需要修改接口
观察者模式:
java
// Java 原生支持(但已过时,推荐自己实现或用 Spring Event)
// 自定义观察者模式
// 1. 定义事件
public record AnimalEvent(String animalName, String action, LocalDateTime time) {}
// 2. 定义监听器接口
@FunctionalInterface
public interface AnimalEventListener {
void onEvent(AnimalEvent event);
}
// 3. 事件发布者
public class AnimalEventManager {
private final List<AnimalEventListener> listeners = new CopyOnWriteArrayList<>();
public void register(AnimalEventListener listener) {
listeners.add(listener);
}
public void unregister(AnimalEventListener listener) {
listeners.remove(listener);
}
public void publish(AnimalEvent event) {
listeners.forEach(listener -> listener.onEvent(event));
}
}
// 4. 使用
AnimalEventManager manager = new AnimalEventManager();
manager.register(event ->
System.out.println("日志记录: " + event.animalName() + " " + event.action()));
manager.register(event ->
System.out.println("通知管理员: " + event.animalName() + " " + event.action()));
manager.publish(new AnimalEvent("狮子", "进食", LocalDateTime.now()));
Spring 中的观察者模式体现:
java
// Spring ApplicationEvent 机制
// 1. 定义事件
public class AnimalCreatedEvent extends ApplicationEvent {
private final String animalName;
public AnimalCreatedEvent(Object source, String animalName) {
super(source);
this.animalName = animalName;
}
}
// 2. 发布事件
@Service
public class AnimalService {
@Autowired
private ApplicationEventPublisher publisher;
public void createAnimal(String name) {
// 业务逻辑...
publisher.publishEvent(new AnimalCreatedEvent(this, name));
}
}
// 3. 监听事件
@Component
public class AnimalCreatedListener {
@EventListener
public void handleAnimalCreated(AnimalCreatedEvent event) {
System.out.println("动物已创建: " + event.getAnimalName());
}
}
// 其他体现:
// - Spring 容器生命周期回调(ContextRefreshedEvent 等)
// - @Transactional 的提交/回滚事件
// - ApplicationReadyEvent 等启动事件
核心知识点总结
- 单例:volatile + DCL 或静态内部类,枚举最安全,理解指令重排序
- 工厂:简单工厂 → 工厂方法 → 抽象工厂,递进关系,开闭原则
- 观察者:发布-订阅解耦,Spring Event 是生产级实现
五、注解(Annotation)是什么?怎么自定义注解 + AOP 实现切面?
面试官会怎么问
"Java 注解的底层原理是什么?它和注释有什么区别?"
追问:"你写过自定义注解吗?怎么配合 AOP 实现一个权限校验?"
再追问:"元注解有哪些?注解的保留策略是什么意思?"
求职者该怎么答
注解的本质:
java
// 注解本质上是一个继承了 java.lang.annotation.Annotation 的接口
// 编译后注解信息保存在 class 文件的 RuntimeVisibleAnnotations 属性中
// 运行时可以通过反射获取注解信息
// 定义一个注解:
public @interface MyAnnotation {
String value(); // 注解属性
}
// 编译后等价于:
public interface MyAnnotation extends Annotation {
String value();
}
// 使用注解:
@MyAnnotation("hello")
public class MyClass { }
// 运行时获取注解:
Class<?> clazz = MyClass.class;
MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
System.out.println(annotation.value()); // "hello"
四个元注解:
java
// 1. @Target ------ 注解可以用在什么地方
@Target({
ElementType.TYPE, // 类、接口、枚举
ElementType.METHOD, // 方法
ElementType.FIELD, // 字段
ElementType.PARAMETER, // 参数
ElementType.CONSTRUCTOR, // 构造器
ElementType.LOCAL_VARIABLE, // 局部变量
ElementType.ANNOTATION_TYPE,// 注解类型
ElementType.PACKAGE // 包
})
// 2. @Retention ------ 注解的生命周期
@Retention(RetentionPolicy.SOURCE) // 只存在于源码,编译后丢弃
@Retention(RetentionPolicy.CLASS) // 存在于 class 文件,运行时不可获取(默认)
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射获取(最常用)
// 3. @Documented ------ 注解是否包含在 Javadoc 中
// 4. @Inherited ------ 子类是否继承父类的注解(仅对类注解有效)
实战:自定义权限校验注解 + AOP:
java
// 第一步:定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireRole {
String[] value(); // 允许的角色列表
}
// 第二步:编写 AOP 切面
@Aspect
@Component
public class RoleCheckAspect {
@Around("@annotation(requireRole)")
public Object checkRole(ProceedingJoinPoint joinPoint, RequireRole requireRole) throws Throwable {
// 1. 获取当前用户角色(实际从 Token/Session 中获取)
String currentRole = getCurrentUserRole();
// 2. 检查是否在允许的角色列表中
boolean hasPermission = Arrays.stream(requireRole.value())
.anyMatch(role -> role.equals(currentRole));
if (!hasPermission) {
throw new UnauthorizedException(
"权限不足,需要角色: " + Arrays.toString(requireRole.value())
+ ",当前角色: " + currentRole
);
}
// 3. 权限通过,执行原方法
return joinPoint.proceed();
}
private String getCurrentUserRole() {
// 模拟:从 SecurityContext 或 JWT 中解析
return "admin";
}
}
// 第三步:在 Controller 上使用
@RestController
public class ZooController {
@RequireRole({"admin", "zookeeper"})
@PostMapping("/animals")
public ResponseEntity<?> addAnimal(@RequestBody AnimalDTO dto) {
// 业务逻辑...
return ResponseEntity.ok().build();
}
@RequireRole({"visitor"})
@GetMapping("/animals")
public ResponseEntity<?> listAnimals() {
// 游客也能看...
return ResponseEntity.ok().build();
}
}
注解 + 参数校验实战:
java
// 自定义日志注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
String module() default "";
String action() default "";
}
// AOP 切面自动记录日志
@Aspect
@Component
public class OperationLogAspect {
@Autowired
private LogService logService;
@Around("@annotation(operationLog)")
public Object recordLog(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
try {
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - startTime;
logService.save(new LogRecord(
operationLog.module(),
operationLog.action(),
methodName,
Arrays.toString(args),
"SUCCESS",
cost
));
return result;
} catch (Exception e) {
logService.save(new LogRecord(
operationLog.module(),
operationLog.action(),
methodName,
Arrays.toString(args),
"FAIL: " + e.getMessage(),
System.currentTimeMillis() - startTime
));
throw e;
}
}
}
// 使用
@OperationLog(module = "动物管理", action = "新增动物")
@PostMapping("/animals")
public ResponseEntity<?> addAnimal(@RequestBody AnimalDTO dto) {
// ...
}
Java 内置常用注解:
java
@Override // 编译期检查方法覆写
@Deprecated // 标记已过时
@SuppressWarnings // 抑制编译器警告
// Java 8+ 新增的类型注解(用于类型检查框架)
@FunctionalInterface // 标记函数式接口
@SafeVarargs // 抑制堆污染警告
// Java 14+ 预览特性
// 注解可以放在 record 的组件上
public record Animal(@NotBlank String name, @Min(0) int age) {}
核心知识点总结
- 注解本质是接口:编译后保存在 class 文件,运行时通过反射读取
- 四个元注解:@Target(作用域)、@Retention(生命周期)、@Documented、@Inherited
- 注解 + AOP = 声明式编程:把横切关注点(日志、权限、事务)从业务代码中解耦
- Spring 大量使用注解:@Autowired、@Service、@Transactional 都是注解驱动
知识图谱总结
Java 常见面试题(三)
│
├── 集合框架进阶
│ ├── ArrayList:动态数组、1.5倍扩容、连续内存
│ ├── LinkedList:双向链表、离散内存、Node节点
│ └── 选型建议:绝大多数场景 ArrayList + 预设容量
│
├── 并发工具 AQS
│ ├── AQS 核心:state + CLH队列 + 模板方法
│ ├── CountDownLatch:计数器,等待N个事件,不可重用
│ ├── Semaphore:信号量,控制并发数,可重用
│ └── 公平 vs 非公平:吞吐量 vs 不饥饿
│
├── Stream API 与 Lambda
│ ├── Lambda 本质:invokedynamic + LambdaMetafactory
│ ├── Stream 管道:中间操作惰性 + 终端操作触发
│ ├── 常用操作:filter/map/collect/groupBy/reduce
│ └── 并行流陷阱:线程安全、数据量阈值、IO阻塞
│
├── 设计模式
│ ├── 单例:DCL + volatile / 静态内部类 / 枚举
│ ├── 工厂:简单工厂 → 工厂方法 → 抽象工厂
│ ├── 观察者:发布-订阅解耦
│ └── Spring Event:生产级观察者模式
│
└── 注解(Annotation)
├── 本质:继承 Annotation 的接口
├── 元注解:@Target / @Retention / @Documented / @Inherited
├── 自定义注解 + AOP:声明式权限校验 / 日志记录
└── Spring 注解驱动:@Autowired / @Transactional 等