二 Java基础面试篇
数据类型
引用类型
类:Class接口:Interface数组:Array枚举:Enum
自动装箱:int -> Integer
自动拆箱:Integer -> int
// 下面代码会先自动拆箱将sum转为int,然后执行 + 操作。然后发生自动装箱操作将int转为Integer操作。
Integer sum = 0; for(int i=1000; i<5000; i++){ sum+=i; }
// 其内部变化如下:
int result = sum.intValue() + i; Integer sum = new Integer(result);
// 由于我们这里声明的sum为Integer类型,在上面的循环中会创建将近4000个无用的Integer对象,在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量。
看起来Integer非常不好。那为什么Java还要有Integer?
因为Integer是int的包装类。Java的绝大部分方法或类都是用来处理类类型对象的,比如ArrayList只能以类作为他的存储对象(所以只能用ArrayList而不能用List)。
泛型中的应用: 此外,泛型中也只能使用引用类型包装类。所以包装类还是很有存在的必要的。
List<Integer> list = new ArrayList<>();
list.add(3);
list.add(1);
list.add(2);
Collections.sort(list);System.out.println(list);
转换中的应用
在Java中,基本类型和引用类型不能直接转换,必须使用包装类实现。例如将 int 转为 String,需要int -> Integer -> String
集合中的应用
集合也是只能存储对象而不能存储基本数据类型。因此想存int,就要存Integer。如果有一个列表,想要求所有元素的和,如果用int,需要一个循环来遍历。如果是Integer包装类,就可以直接用stream()
方法计算所有元素的和。
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
int sum = list.stream().mapToInt(Integer::intValue).sum();
Integer的缓存
Java的Integer类内部实现了一个静态缓存池,用于存储特定范围内的整数值对应的Integer对象。这个范围为-128~127
。当通过Integer.valueOf(int)
方法创建一个在这个范围内的整数对象时,会复用缓存中的现有对象,直接从内存中取出,不用重新new。
面向对象
面向对象的三大特征:
- 封装
- 继承
- 多态
- 重载 (编译时多态)
- 重写 (运行时多态)
多态体现在哪些方面?
- 方法重载:
- 同一类中可以有多个同名方法,且有不同的参数列表。(eg, add(int a, int b) 以及add(int a, int b, int c)
- 方法重写:
- 子类能够提供对父类中同名方法的具体实现。在运行时,JVM会根据对象的数据类型确定调用哪个版本的方法。(eg, 在一个动物类中,定义一个
sound
方法, 子类Dog
可以重写该方法实现bark
,而Cat
可以实现meow
)
- 子类能够提供对父类中同名方法的具体实现。在运行时,JVM会根据对象的数据类型确定调用哪个版本的方法。(eg, 在一个动物类中,定义一个
- 接口的实现
- 多个类可以实现同一个接口,并用接口的引用来调用这些类的方法。(eg, 多个类,如Cat Dog 都实现了 Animal 接口,当用 Animal 类型的引用来调用 makeSound 方法时,会出发对应的实现)
- 向上转型和向下转型
- 向上转型:使用父类类型的引用指向子类对象。向上转型可以在运行时期采用不同的子类实现。
- 向下转型:将父类引用转回子类类型。但在执行前需要确认引用实际指向的对象类型来避免
ClassCastException
多态解决了什么问题?
多态指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。
多态可以提高代码的扩展性和复用性。是很多设计模式、设计原则、编程技巧的代码实现基础。如策略模式、基于接口而非实现变成、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等。
重载和重写有什么区别?
重载是在同一个类中定义多个同名不同参数方法。
重写是子类重新定义父类中的方法。
抽象类和普通类的区别
- 实例化:普通类可以直接实例化对象,而抽象类不能被实例化。
- 方法实现:普通类中的方法可以有具体的实现,抽象类中的方法可以有实现也可以没有实现。
- 继承:一个类可以继承一个普通类 & 多个接口;一个类只能继承一个抽象类 & 多个接口
- 实现限制:普通类可以被其他类继承和使用,而抽象类一般用于作为基类,需要被其他类继承和扩展使用。 (因此抽象类不能被final修饰,因为final修饰符禁止类被继承或方法被重写)
抽象类和接口的区别
两者的特点
- 抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于有明显继承关系的场景。
- 接口用于定义行为规范,可以多实现,只能有常量、抽象方法、默认方法和静态方法。
两者的区别
- 实现方式:一个类可以实现多个接口,继承一个抽象类。所以使用接口可以间接的实现多重继承。
- 方法方式:接口只能有定义不能有方法的实现。java1.8中引入定义default方法体。抽象类可以有定义+实现。
- 访问修饰符:接口的成员变量默认为public static final,必须赋初值,不能被修改。其所有成员方法都是public abstract的。抽象类的成员变量默认为default。抽象方法由于被abstract修饰不能被private, static, synchronized, native修饰。抽象方法必须以分号结尾,不能带花括号。
- 变量:抽象类中可以包含实例变量和静态变量,接口只能包含常量(静态常量)
接口里可以定义哪些方法? 不能定义具体方法体吗?
- 抽象方法:在接口中定义的方法默认都是public abstract 修饰的。这些修饰符可以省略。
- 默认方法:用default修饰,就可以允许接口提供具体实现。实现类可以选择重写默认方法
public interface Animal {void makeSound();default void sleep() {System.out.println("Sleeping...");}
}
- 私有方法:私有方法是在Java 9 中引用,用于在接口中为默认方法或其他私有方法提供辅助功能。私有方法不能被实现类访问,只能在接口内部使用。
public interface Animal {void makeSound();default void sleep() {System.out.println("Sleeping...");}private void logSleep() {System.out.println("Logging sleep");}
}
解释Java中的静态变量和静态方法
静态变量和静态方法是与类本身关联的,而不是与类的实例(对象)关联。他们在内存中只有一份,可以被类的所有实例共享。
静态是用static修饰的。
静态变量
- 共享性:所有该类的实例共享同一个静态变量。
- 初始化:静态变量在类被加载时初始化,只会对其分配一次
- 访问方式:可以直接通过类名方式访问。推荐。
静态方法
- 无实例依赖:可以直接通过类名访问,无需创建类实例。静态方法不能直接访问非静态的成员变量或方法。
- 访问静态成员:静态方法可以直接访问静态变量和静态方法。但是不能直接访问非静态成员。
- 多态性:静态方法不支持重写,可以被隐藏。
使用场景
静态变量:常用于在所有对象间共享的数据。如计数器,常量等。
静态方法:常用于助手方法,获取类级别的信息或者是没有依赖于实例的数据处理。
// 静态变量如何访问非静态变量和方法
class Car {// 非静态变量和方法String model = "Toyota";public void displayModel() {System.out.println("Model: " + model);}// 静态方法public static void showCarInfo() {// 不能直接访问非静态变量和方法// System.out.println(model); // 编译错误// displayModel(); // 编译错误// 通过实例访问非静态变量和方法Car car = new Car(); // 创建对象System.out.println(car.model); // 访问非静态变量car.displayModel(); // 调用非静态方法}
}public class Main {public static void main(String[] args) {// 调用静态方法Car.showCarInfo();}
}
有一个父类和子类,都有静态的成员变量、静态构造方法和静态方法,在我new一个子类对象的时候,加载顺序是怎么样的?
- 父类静态成员变量、静态代码块(如果有)
- 子类静态成员变量、静态代码块(如果有)
- 父类构造方法(实例化对象时)
- 子类构造方法(实例化对象时)
对象
Java创建对象除了 new 还有什么方式?
- 通过反射创建对象。
可以使用 Class 类的 newInstance() 方法或者通过 Constructor 类来创建对象呢
public class MyClass {public MyClass() {// Constructor}
}
public class Main {public static void main(String[] args) throws Exception {Class<?> clazz = MyClass.class;MyClass obj = (MyClass) clazz.newInstance();}
}
- 通过反序列化创建对象:
通过将对象序列化(保存到文件或网络传输),然后再反序列化(从文件或网络传输中读取对象)的方式来创建对象,对象能被序列化和反序列化的前提是实现Serializable接口
import java.io.*;public class MyClass implements Serializable {// Class definition
}public class Main {public static void main(String[] args) throws Exception {// Serialize objectMyClass obj = new MyClass();ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.ser"));out.writeObject(obj);out.close();// Deserialize objectObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));MyClass newObj = (MyClass) in.readObject();in.close();}
}
New出的对象什么时候回收?
Java的对象回收机制是由垃圾回收器根据一些算法决定的,会周期性地检测不再被引用的对象,并将其回收,释放内存。具体的算法有:
- 引用计数法:引用计数为0就回收
- 可达性分析算法:从根对象出发,通过对象之间的引用链遍历,如果存在一条引用链到达某个对象,则说明该对象是可达的。反之就回收这个对象
反射
反射机制是在运行状态中,对于任意一个类,都能知道这个类中的所有属性和方法,对于任意一个对象,都能调用它的方法和属性。这种动态获取的信息和动态调用对象方法的功能称为Java的反射机制。
反射具有如下特性:
- 运行时类信息访问:反射机制允许程序在运行时获取类的完整结构信息,包括类名,包名,父类,实现的接口,构造函数,方法和字段等。
- 动态对象创建:可以使用反射API动态地创建对象实例,即使在编译时不知道具体的类名。这是通过Class类的newInstance()方法或Constructor对象的newInstance()方法实现的。
- 动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过Method类的invoke()方法实现。允许你传入对象实例和参数值来执行方法。
- 访问和修改字段值:私有字段也可以访问和修改。这是通过Field类的get()和set()方法完成的。
平时写代码和框架中发射有哪些应用场景?
- 加载数据库
项目可能有的用mysql,有的用oracle,需要动态地根据实际情况加载驱动类。这个时候在使用JDBC连接数据库时,使用Class.forName()通过反射加载数据库的驱动程序,如果是mysql则传入mysql的驱动类,而如果是oracle则传入的参数就变成另一个
// DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
Class.forName("com.mysql.cj.jdbc.Driver");
- 配置文件加载
Spring框架的IOC控制反转(动态加载管理bean),Spring通过配置文件配置各种各样的bean,需要什么就配什么,spring容器会根据你的需求动态加载。
Spring通过XML配置模式装载Bean的过程:
- 将程序中所有的XML或properties配置文件加载入内存
- Java类里面解析xml或者properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
- 使用反射机制,根据这个字符串获取某个类的Class实例
- 动态配置实例的属性。
eg:
配置文件:
className=com.example.reflectdemo.TestInvoke
methodName=printlnState
实体类:
public class TestInvoke {private void printlnState() {System.out.println("I am fine");}
}
解析配置文件内容:
public static String getName(String key) throws IOException {// 创建 Properties 对象Properties properties = new Properties();// 读取 properties 文件FileInputStream in = new FileInputStream("D:\\language-specification\\src\\main\\resources\\application.properties");properties.load(in);in.close();// 根据 key 获取属性值return properties.getProperty(key);
}
利用反射获取实体类的Class实例,创建实体类的实例对象,调用方法
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, ClassNotFoundException, InstantiationException {// 使用反射机制获取 Class 对象Class<?> c = Class.forName(getName("className"));System.out.println(c.getSimpleName());// 获取类中的方法Method method = c.getDeclaredMethod(getName("methodName"));// 绕过安全检查method.setAccessible(true);// 创建实例对象TestInvoke testInvoke = (TestInvoke) c.newInstance();// 调用方法method.invoke(testInvoke);
}
注解
注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。
我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。
(当你调用一个注解的方法时,实际上是通过动态代理对象调用AnnotationInvocationHandler
的invoke
方法,这个方法不是直接执行单吗返回结果,而是从一个内部的Map
,即memberValues中查找与注解方法对应的值。)
异常
Java的异常类层次结构:
Java的异常主要基于Throwable类及其子类。
- Error(错误):表示运行时环境的错误。错误是指程序无法处理的严重问题,如系统崩溃,虚拟机错误,动态连接失败等。通常,程序不应该捕获这类错误,如
OutOfMemoryError, StackOverflowError
等。 - Exception(异常):表示程序本身可以处理的异常条件,分为两大类
- 非运行时异常:这类异常中编译时就必须被捕获或声明抛出。他们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。
- 运行时异常:这类包括运行时异常(RuntimeException)和错误(Error)。运行时异常由程序错误导致,如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。运行时异常是不需要在编译时强制捕获或声明的。
Java的异常处理有哪些?
- try-catch语句块:用于捕获并处理可能抛出的异常。try块中包含可能抛出的异常的代码,catch块用于捕获并处理特定类型的异常。可以有多个catch块来处理不同类型的异常
try {// 可能抛出异常的代码块
} catch (ExceptionType1 e1) {// 处理异常类型1的逻辑
} catch (ExceptionType2 e2) {// 处理异常类型2的逻辑
} catch (ExceptionType3 e3) {// 处理异常类型3的逻辑
} finally {// 可选的finally块,用于无论发生何种异常都需要执行的代码
}
- throw语句:用于手动抛出异常。可以根据需要在代码中使用throw语句主动抛出特定类型的异常。
throw new ExceptionType("Exception message");
- throws关键字:用于在方法生命中声明可能抛出的异常类型。如果一个方法可能抛出异常,但不想再方法内部处理,可以使用throws关键字将异常传递给调用者来处理
public void methodName() throws ExceptionType {// 方法体
}
- finally块:用于定义无论是否发生异常都会执行的代码块。通常用于释放资源,确保资源的正确关闭。
try {// 可能抛出异常的代码
} catch (ExceptionType e) {// 处理异常的逻辑
} finally {// 无论是否发生异常都会执行的代码
}
抛出异常什么时候不用throws?
如果异常是未检查异常或者在方法内部被捕获和处理了,那就不需要用throws。
- Unchecked Exceptions: 未检查异常是继承RuntimeException类和Error类的异常,编译器不强制要求异常处理,如NullPointerException, ArrayIndexOutOfBoundsException等。
- 捕获和处理异常:如果在方法内部捕获了可能出现的异常并在方法内部处理他们,就不用在方法签名处throws
try{return "a"} finally {return "b"}
这条语句返回什么?
finally块中的return语句会覆盖try中的return返回,因此将返回 b
object
== 和 equal有什么区别?
- 对于字符串变量,使用
==
和 equals比较字符串,其比较方法不同。==
比较两个变量本身的值,即两个对象在内存中的首地址,equals比较字符串中包含的内容是否相同 - 对于非字符串变量,== 和 equals 的作用一样,都是比较首地址,即两个引用变量是否指向同一个对象。
总结:
- ==:比较的是字符串内存地址是否相同,属于地址比较
- equals():比较的是两个字符串的内容,属于内容比较。
StringBuffer和StringBuild的区别是什么?
- StringBuffer:是线程安全的,适用于多线程
- StringBuilder:线程不安全的,适用于单线程
速度:
从快到慢依次为:StringBuilder > StringBuffer > String
Java 1.8 新特性
Java中的stream的API简单介绍
案例1: 过滤并收集满足条件的元素
l
List<String> originalList = Arrays.asList("apple", "fig", "banana", "kiwi);
List<String> filteredList = originalList.stream().filter(s -> s.length() > 3).collect(Collectors.toList());
/*这里可以直接在原始列表上调用 .stream() 方法创建一个流,使用 .filter() 中间操作筛选出长度大于3的字符串,最后使用 .collect(Collectors.toList()) 终端操作将结果收集到一个新的列表中。
*/
案例2:计算列表中所有数字的和
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);int sum = numbers.stream().mapToInt(Integer::intValue).sum();
通过stream API,可以先使用 .mapToInt() 将Integer流转为IntStream(这是为了搞笑处理基本类型),然后直接调用 .sum() 方法来计算总和。
Stream流的并行API是什么?
Stream流的并行API是 ParallelStream
并行流(ParallelStream) 就是将元数据分为多个子流对象进行多线程操作,然后将处理的结果再汇总为一个流对象,底层使用通用的fork/join池来实现,即将一个任务拆分为多个小任务并行计算,再把多个小任务的结果合并成总的计算结果。
Stream’串行流和并行流的主要区别:
对于cpu密集型,适用于并行流。
对于i/o密集型且任务数相对线程数较大,那么直接用ParallelStream并不是很好的选择。
completableFuture怎么用的?
completableFuture是由Java 8 引用的,在Java8之前一般通过Future实现异步。
Future用于表示异步计算的结果,只能通过阻塞或者轮询的方式获取结果,而且不支持回调方法。每一步的处理逻辑都嵌套在前一步的回调函数中,增加了理解和维护的难度。
CompletableFuture对Future进行了扩展,可以通过设置回调的方式处理计算结果,同时也支持组合操作,支持进一步的编排,同时一定程度解决了回调地狱的问题。
CompletableFuture的实现如下:
// 创建一个具有5个线程的固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);// 创建第一个异步任务:打印并返回"step1 result"
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {System.out.println("执行step 1");return "step1 result";
}, executor);// 创建第二个异步任务:打印并返回"step2 result"
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {System.out.println("执行step 2");return "step2 result";
}, executor);// 将两个异步任务的结果组合,当两个任务都完成后执行
cf1.thenCombine(cf2, (result1, result2) -> {// 打印两个任务的结果System.out.println(result1 + " + " + result2);System.out.println("执行step 3");// 返回新的结果,供下一步使用return "step3 result";
}).thenAccept(result3 -> {// 接收最终的结果并打印System.out.println(result3);
});
序列化
怎么把一个对象从一个jvm转移到另一个jvm?
- 使用序列化和反序列化。将对象序列化为字节流,并将其发送到另一个JVM,然后在另一个JVM中反序列化字节流恢复对象。可以通过Java的 ObjectOutputStream 和ObjectInputStream实现。
- 使用消息传递机制:利用消息队列(RabbitMQ、Kafka)或者通过网络套接字进行通信,将对象从一个JVM发送到另一个。这需要自定义协议来序列化对象并在另一个JVM中反序列化。
- 使用远程方法调用(RPC):可以使用远程方法调用框架,如gRPC,来实现对象在不同JVM之间的传输。远程方法调用可以在分布式系统中调用远程JVM上的对象的方法。
- 使用共享数据库或缓存。将对象存储在共享数据库(Mysql, PostgreSQL)或共享缓存(如Redis)中,让不同的JVM可以访问这些共享数据。这种方法适用于需要共享数据但不需要直接传输数据的场景。
序列化和反序列化让你自己实现你会怎么实现?
Java默认的序列化虽然实现方便,但却存在安全漏洞,不跨语言以及性能差等缺陷。
所以选择用主流序列化框架更好,如FastJson,Protobuf来替代Java序列化。
如果追求性能的话,Protobuf 序列化框架会比较合适,Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好, 在编码和解码的性能方面也很高效。Protobuf 的编码和解码过程结合.proto 文件格式,加上 Protocol Buffer 独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说 Protobuf 的整体性能非常优秀。
将对象转为二进制字节流具体怎么实现?
其实是定义了序列化后,反序列化前的字节流格式,然后对此格式进行操作,生成符合格式的字节流或者将字节流解析为对象。
在Java中通过序列化对象流来完成序列化和反序列化
- ObjectOutputStream:通过writeObject() 方法做序列化操作。
- ObjectInputStream:通过readObject() 方法做反序列化操作。
只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常。
实现对象序列化的步骤:
- 让类实现Serializable接口:
import java.io.Serializable;public class MyClass implements Serializable {// class code
}
- 创建输出流并写入对象
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;// 创建MyClass类型的对象
MyClass obj = new MyClass();try {// 创建一个FileOutputStream来指向一个文件,这里的文件名为"object.ser"FileOutputStream fileOut = new FileOutputStream("object.ser"); // .ser一般用于表示序列化对象的文件// 创建一个ObjectOutputStream,它封装了FileOutputStream// ObjectOutputStream用于将对象写入到文件输出流ObjectOutputStream out = new ObjectOutputStream(fileOut);// 使用ObjectOutputStream的writeObject方法将obj对象序列化并写入到文件中out.writeObject(obj);// 关闭ObjectOutputStream流out.close();// 关闭FileOutputStream流fileOut.close();
} catch (IOException e) {// 捕获IOException异常,如果在打开、写入或关闭文件过程中出现问题,将执行此块e.printStackTrace();
}
- 实现对象反序列化:创建输入流并读取对象
import java.io.FileInputStream;
import java.io.ObjectInputStream;// 声明一个 MyClass 类型的变量,初始为 null
MyClass newObj = null;try {// 创建一个 FileInputStream 来从指定的文件(这里是 "object.ser")读取数据FileInputStream fileIn = new FileInputStream("object.ser");// 创建一个 ObjectInputStream,它封装了 FileInputStream// ObjectInputStream 用于从文件中读取并恢复对象ObjectInputStream in = new ObjectInputStream(fileIn);// 使用 ObjectInputStream 的 readObject 方法读取先前序列化的对象// 需要将返回的 Object 类型强制转换为 MyClass 类型newObj = (MyClass) in.readObject();// 关闭 ObjectInputStreamin.close();// 关闭 FileInputStreamfileIn.close();
} catch (IOException | ClassNotFoundException e) {// 捕获 IOException 或 ClassNotFoundException 异常// IOException 可能由文件操作引发,如文件不存在、数据不完整等// ClassNotFoundException 由于找不到序列化对象的类定义而抛出e.printStackTrace();
}
设计模式
volatile和sychronized如何实现单例模式?(volatile和synchronized都是保证同步的。volatile更轻便,synchronized成为重量级锁)
public class SingleTon {// volatile 关键字修饰变量 防止指令重排序private static volatile SingleTon instance = null;private SingleTon(){}public static SingleTon getInstance(){if(instance == null){//同步代码块 只有在第一次获取对象的时候会执行到 ,//第二次及以后访问时 instance变量均非null故不会往下执行了 直接返回啦synchronized(SingleTon.class){if(instance == null){instance = new SingleTon();}}}return instance;}
}
正确的双重检查锁定模式需要使用volatile。volatile包含两个功能:
- 保证可见性。使用volatile定义的变量,将会保证对所有的线程的可见性。
- 禁止指定重排序优化。
由于volatile禁止对象创建时指定之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
代理模式和适配器模式有什么区别?(代理模式和适配器模式都是Java设计模式中的)
适配器模式:将一个接口转换成客户希望的另一个接口,使原本不兼容的接口类可以一起工作
代理模式:给一个对象提供一个代理对象,并由代理对象控制对原对象的引用,使客户不能直接与真正的目标对象通信
结构不同:代理模式一般包含抽象主题,真实主题和代理 三种角色,适配器模式包含目标接口,适配器和被适配者 三种角色
应用场景不同:代理模式常用于添加额外功能或控制对对象的访问,适配器模式常用于让不兼容的接口协同工作。
I/O (⭐️⭐️看不懂)
BIO、NIO、AIO区别是什么?
- BIO(blocking IO):就是传统的java.io包,是基于流模型实现的,交互方式是同步、阻塞方式,也就是说在读入输入流或输出流时,在读写动作完成前,线程会一直阻塞在哪里,他们之间的调用是可靠的线性顺序
- NIO(non-blocking IO):是java 1.4引入的java.nio包。可以构建多路复用、同步非阻塞的IO程序,同时提供了更接近操作系统底层的数据操作方式。
- AIO(Asynchronous IO):是Java1.7后引入的NIO的升级版本,提供了异步非阻塞的IO操作方式,是基于事件和回调机制实现的。应用操作后会直接返回,不会阻塞在那里,当后台处理完成后操作系统会通知响应线程进行后续操作。
Java 是如何实现网络IO高并发编程的?
可以使用Java NIO,是一种同步非阻塞的I/O模型,也是I/O多路复用的基础
NIO是基于I/O多路复用实现的,它可以只用一个线程处理多个客户端I/O,如果需要管理成千上万个连接,但是每个连接只发送少量数据例如一个聊天服务器,用NIO实现会好点
NIO 是如何实现的?
NIO是一种同步非阻塞的IO模型,所以也可以叫NON-BLOCKINGIO。(同步是指线程不断轮询IO事件是否就绪,非阻塞是指线程在等待IO的时候,可以同时做其他任务。)
NIO主要有三个核心部分:Channel(通道)、Buffer(缓冲区),Selector(选择区)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作。数据总是从通道读取到缓冲区中,或从缓冲区写入到通道。Selector用于监听多个通道事件(如连接打开,数据到达)。因此单个线程可以监听多个数据通道。
NIO有一个专门的线程处理所有的IO时间,并负责分发。事件驱动机制,事件到来时出发操作,不需要阻塞的监视时间,线程之间通过wait,notify通信,减少线程切换。
哪个框架用到了NIO?
Netty。
其他问题
有一个学生类,想按照分数降序排序,再按照学号升序排序,如何实现?
可以使用Comparable接口来实现按照分数排序,再按照学号排序。
public class Student implements Comparable<Student> {private int id;private int score;// 构造方法和其他属性、方法省略@Overridepublic int compareTo(Student other) {if(this.score != other.score)return Integer.compare(other.score, this.score); // 按照分数降序排序elsereturn Integer.compare(this.id, other.id); // 如果分数相同,则按照学号升序排序}
}
然后在需要对学生列表排序的地方,使用Collections.sort() 方法对学生列表进行排序即可。
List<Student> students = new ArrayList<>();
// 添加学生对象列表中
Collections.sort(students);
如何对一个List进行排序?
- 使用 Collections.sort()
如果列表元素实现了Comparable接口,那么可以直接使用它们的自然顺序进行排序。你也可以提供一个自定义的Comparator来指定排序的具体规则。
// 使用元素自然顺序
List<Integer> list = new ArrayList<>(Arrays.asList(5, 3, 1, 4, 2));
Collections.sort(list);
System.out.println(list); // 输出排序后的列表: [1, 2, 3, 4, 5]//使用自定义比较器
List<String> list = new ArrayList<>(Arrays.asList("banana", "apple", "cherry"));
Collections.sort(list, new Comparator<String>() {public int compare(String o1, String o2) {return o1.length() - o2.length(); // 根据字符串长度排序}
});
System.out.println(list); // 输出排序后的列表: ["apple", "banana", "cherry"]
- 使用 List.sort()
// 使用元素自然顺序
List<Integer> list = new ArrayList<>(Arrays.asList(5, 3, 1, 4, 2));
list.sort(null); // 等同于 list.sort(Comparator.naturalOrder());
System.out.println(list); // 输出: [1, 2, 3, 4, 5]//使用自定义比较器
List<String> list = new ArrayList<>(Arrays.asList("banana", "apple", "cherry"));
list.sort((s1, s2) -> s1.length() - s2.length()); // 使用Lambda表达式简化Comparator的编写
System.out.println(list); // 输出: ["apple", "banana", "cherry"]
Native方法解释一下
在Java中,native方法是一种特殊类型的方法, 他允许Java代码调用外部的本地代码,如C C++编写的代码。
在Java类中,native方法看起来和其他方法类似,只是其方法体由native关键字代替,没有实际的实现代码。例如:
public class NativeExample {public native void nativeMethod();
}
实现Native方法需要完成的步骤:
- 生成JNI头文件
- 编写本地代码:用别的语言
- 编译本地代码:将C/C++代码编译成动态链接库(DLL for Windows),共享库(SO for linux)
- 加载本地库:在Java中使用System.loadLibrary()方法来加载你编译好的本地库,这样JVM就可以找到并调用native方法的实现。