小林Coding—Java「二、Java基础篇」

󠀲󠀲二 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)
  • 接口的实现
    • 多个类可以实现同一个接口,并用接口的引用来调用这些类的方法。(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一个子类对象的时候,加载顺序是怎么样的?

  1. 父类静态成员变量、静态代码块(如果有)
  2. 子类静态成员变量、静态代码块(如果有)
  3. 父类构造方法(实例化对象时)
  4. 子类构造方法(实例化对象时)

对象

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 object
        MyClass obj = new MyClass();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.ser"));
        out.writeObject(obj);
        out.close();

        // Deserialize object
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));
        MyClass newObj = (MyClass) in.readObject();
        in.close();
    }
}

New出的对象什么时候回收?

Java的对象回收机制是由垃圾回收器根据一些算法决定的,会周期性地检测不再被引用的对象,并将其回收,释放内存。具体的算法有:

  1. 引用计数法:引用计数为0就回收
  2. 可达性分析算法:从根对象出发,通过对象之间的引用链遍历,如果存在一条引用链到达某个对象,则说明该对象是可达的。反之就回收这个对象

反射

反射机制是在运行状态中,对于任意一个类,都能知道这个类中的所有属性和方法,对于任意一个对象,都能调用它的方法和属性。这种动态获取的信息和动态调用对象方法的功能称为Java的反射机制。

反射具有如下特性:

  1. 运行时类信息访问:反射机制允许程序在运行时获取类的完整结构信息,包括类名,包名,父类,实现的接口,构造函数,方法和字段等。
  2. 动态对象创建:可以使用反射API动态地创建对象实例,即使在编译时不知道具体的类名。这是通过Class类的newInstance()方法或Constructor对象的newInstance()方法实现的。
  3. 动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过Method类的invoke()方法实现。允许你传入对象实例和参数值来执行方法。
  4. 访问和修改字段值:私有字段也可以访问和修改。这是通过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的过程:

  1. 将程序中所有的XML或properties配置文件加载入内存
  2. Java类里面解析xml或者properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
  3. 使用反射机制,根据这个字符串获取某个类的Class实例
  4. 动态配置实例的属性。

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常量池。
(当你调用一个注解的方法时,实际上是通过动态代理对象调用AnnotationInvocationHandlerinvoke方法,这个方法不是直接执行单吗返回结果,而是从一个内部的Map,即memberValues中查找与注解方法对应的值。)

异常

Java的异常类层次结构:
在这里插入图片描述
Java的异常主要基于Throwable类及其子类。

  1. Error(错误):表示运行时环境的错误。错误是指程序无法处理的严重问题,如系统崩溃,虚拟机错误,动态连接失败等。通常,程序不应该捕获这类错误,如OutOfMemoryError, StackOverflowError等。
  2. Exception(异常):表示程序本身可以处理的异常条件,分为两大类
    1. 非运行时异常:这类异常中编译时就必须被捕获或声明抛出。他们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。
    2. 运行时异常:这类包括运行时异常(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();

    // 关闭 ObjectInputStream
    in.close();

    // 关闭 FileInputStream
    fileIn.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;
	
	// 构造方法和其他属性、方法省略

	@Override
	public int compareTo(Student other) {
		if(this.score != other.score)
			return Integer.compare(other.score, this.score);	// 按照分数降序排序
		else
			return Integer.compare(this.id, other.id);	// 如果分数相同,则按照学号升序排序
	}
}

然后在需要对学生列表排序的地方,使用Collections.sort() 方法对学生列表进行排序即可。

List<Student> students = new ArrayList<>();
// 添加学生对象列表中
Collections.sort(students);

如何对一个List进行排序?

  1. 使用 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"]

  1. 使用 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方法需要完成的步骤:

  1. 生成JNI头文件
  2. 编写本地代码:用别的语言
  3. 编译本地代码:将C/C++代码编译成动态链接库(DLL for Windows),共享库(SO for linux)
  4. 加载本地库:在Java中使用System.loadLibrary()方法来加载你编译好的本地库,这样JVM就可以找到并调用native方法的实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值