> 文章列表 > Java面试题总结 | Java基础部分2(持续更新)

Java面试题总结 | Java基础部分2(持续更新)

Java面试题总结 | Java基础部分2(持续更新)

文章目录

    • 反射的优缺点
    • BIO、AIO、NIO
      • 同步异步概念
      • **阻塞与非阻塞**
      • **BIO**
      • **NIO**
      • **AIO**
      • 总结
    • 设计模式的好处
    • 设计模式一定是好用的吗
    • Integer.ValueOf和new Integer的区别
    • Integer.parseInt(s)与Integer.valueOf(s)的区别
    • String是线程安全的吗?
    • StringBuffer和StringBuilder的区别
    • String、StringBuffer、StringBuilder的性能
    • String#equals() 和 Object#equals() 有何区别?
    • String不可边的好处
    • List<? super T>和List<? extends T>有什么区别?
    • Object类里面有哪些常用的方法
    • == 和 equals() 的区别
    • HashCode和equals
    • 为什么要提供hash和equals两种方法?
    • Java里面的异常类了解吗?
    • JDK JVM JRE的关系区别
    • Math类的取整函数
    • 双亲委派机制
    • 自定义加载器如何写
    • 抽象类
    • 抽象类和接口的区别
    • 抽象类和接口的使用场景
    • IO流
    • final和finally的区别
    • 泛型

反射的优缺点

动态获取程序信息以及动态调用对象的功能称为 Java 语言的反射机制。

  • 程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息;
  • 程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员;
  • 程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象。

在运行时判断任意一个对象所属的类; 在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和 方法;在运行时调用任意一个对象的方法;生成动态代理。

好处:反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。

不过,反射让我们在运行时有了分析操作类的能力的同时,也增加了安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。

BIO、AIO、NIO

同步异步概念

同步和异步关注的是*消息通信机制* (synchronous communication/ asynchronous communication)。同步,就是调用某个东西是,调用方得等待这个调用返回结果才能继续往后执行。异步,和同步相反 当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态来通知调用者,或通过回调函数处理这个调用。

阻塞与非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态. 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到 结果之后才会返回。 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

BIO

Java面试题总结 | Java基础部分2(持续更新)

img

Java BIO: 同步并阻塞 (传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不作任何事情会造成不必要的线程开销。

BIO问题分析

  1. 每个请求都需要创建独立的线程,与对应的客户端进行数据处理。

  2. 当并发数大时,需要 创建大量线程来处理连接 ,系统资源占用较大。

  3. 连接建立后,如果当前线程暂时没有数据可读,则当前线程会一直阻塞在 Read 操作上,造成线程资源浪费。

在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量

NIO

Java面试题总结 | Java基础部分2(持续更新)

Java NIO: 同步非阻塞 ,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求会被注册到多路复用器上,多路复用器轮询到有 I/O 请求就会进行处理。具体做法是多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。 用户进程也需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问。

img

NIO BIO 对比

  1. BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。

  2. BIO 是阻塞的,而 NIO 是非阻塞的。

  3. BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中

AIO

AIO (异步非阻塞 I/O) 用户进程只需要发起一个 IO 操作然后立即返回,等 IO 操作真正的完成以后,应用程序会得到 IO 操作完成的通知。

img

总结

Java面试题总结 | Java基础部分2(持续更新)

BIO、NIO、AIO适用场景分析

  1. BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求 比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。

  2. NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。

  3. AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务 器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。

长连接指建立Socket连接后不管是否使用都保持连接,但安全性较差。

设计模式的好处

  • 代码重用性 (即:相同功能的代码,不用多次编写)
  • 可读性 (即:编程规范性, 便于其他程序员的阅读和理解)
  • 可扩展性 (即:当需要增加新的功能时,非常的方便,称为可维护)
  • 可靠性 (即:当我们增加新的功能后,对原来的功能没有影响)
  • 使程序呈现高内聚,低耦合的特性

设计模式一定是好用的吗

设计模式是用来解决某一特定问题的通用解决方案

过度的设计可能会导致程序变得复杂

Integer.ValueOf和new Integer的区别

  • new Integer() 每次都会新建一个对象;
  • Integer.valueOf() :可以将基本类型int转换为包装类型Integer;
    • 在-128-127之间会使用缓存池中的对象,多次调用会取得同一个对象的引用。
    • 大于128的数首先会新建一个对象再去引用
Integer x = new Integer(100);
Integer y = new Integer(100);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(100);
Integer k = Integer.valueOf(100);
System.out.println(z == k);   // true

Integer.parseInt(s)与Integer.valueOf(s)的区别

  • Integer.parseInt(s)多次解析同一个字符串得到的int基本类型数据是相等的,可以直接通过“==”进行判断是否相等。
  • Integer.valueOf(s)多次解析相同的一个字符串时,得到的是Integer类型的对象,得到的对象有时是同一个对象,有时是不同的对象,要根据把s字符串解析的整数值的大小进行决定:如果s字符串对应的整数值在 -128~ 127之间,则解析出的Integer类型的对象是同一个对象;如果s字符串对应的整数值不在-128~127之间,则解析出的Integer类型的对象不是同一个对象。不管对象是否相等,对象中的value值是相等的。

String是线程安全的吗?

String这个类是被final修饰的,String类的值是保存在value数组中的,并且value数组是被private final修饰的,并且String 类没有提供修改这个字符串的方法。

1、private修饰,表明外部的类是访问不到value的,同时子类也访问不到,当然String类不可能有子类,因为类被final修饰了
2、final修饰,表明value的引用是不会被改变的,而value只会在String的构造函数中被初始化,而且并没有其他方法可以修改value数组中的值,保证了value的引用和值都不会发生变化

所以我们说String类是不可变的。

为什么要有常量池?

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

新版的变化

Java 9 将 String 的底层实现由 char[] 改成了 byte[]

StringBuffer和StringBuilder的区别

StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。

StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

StringBuilderStringBuffer 在缓冲区上的区别:

StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。而

StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。

String、StringBuffer、StringBuilder的性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。

StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。

相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

String#equals() 和 Object#equals() 有何区别?

String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Objectequals 方法是比较的对象的内存地址。

String不可边的好处

  1. 方便了实现字符串常量池

  2. String 经常作为参数,String 不可变性可以保证参数不可变

    譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。

  3. 保证了线程的安全

  4. 加快字符串处理速度,因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

List<? super T>和List<? extends T>有什么区别?

参考答案

  • ? 是类型通配符,List<?> 可以表示各种泛型List的父类,意思是元素类型未知的List;
  • List<? super T> 用于设定类型通配符的下限,此处 ? 代表一个未知的类型,但它必须是T的父类型;
  • List<? extends T> 用于设定类型通配符的上限,此处 ? 代表一个未知的类型,但它必须是T的子类型。

扩展阅读

在Java的早期设计中,允许把Integer[]数组赋值给Number[]变量,此时如果试图把一个Double对象保存到该Number[]数组中,编译可以通过,但在运行时抛出ArrayStoreException异常。这显然是一种不安全的设计,因此Java在泛型设计时进行了改进,它不再允许把 List<Integer> 对象赋值给 List<Number> 变量。

数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型,但G<Foo> 不是 G<Bar> 的子类型。Foo[]自动向上转型为Bar[]的方式被称为型变,也就是说,Java的数组支持型变,但Java集合并不支持型变。Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。

Object类里面有哪些常用的方法

wait notifyAll notify getClass hashcode equals clone toString

== 和 equals() 的区别

== 对于基本类型和引用类型的作用效果是不同的:

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。

equals() 方法存在两种使用情况:

  • 类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Objectequals()方法。
  • 类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

HashCode和equals

equals如果不重写的话,默认的是“==”,对于引用对象来说比较的是内存地址,在业务中通过比较的是内容是否相等,所以需要重写equals

如果equals相等的话,那么根据java中的规则,对象的hashcode也是相等的

Hashcode方法是返回对象的内存地址映射为hash值,所以如果没有重写hashCode()方法,任何对象的hashCode()方法都是不相等的。

为什么要提供hash和equals两种方法?

这是因为在一些容器(比如 HashMapHashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HastSet的过程)!

我们在前面也提到了添加元素进HastSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。

Java里面的异常类了解吗?

Exception 和Error都是继承于Throwable 类,在Java中只有Throwable类型的实例才能被程序抛出(throw)或者捕获(catch),它是异常处理机制的基本类型

Error,它表示程序无法处理的错误。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。会终止JVM的运行

Exception,它表示程序可能捕捉或者程序可以处理的异常。其中异常类 Exception 又分为运行时异常(RuntimeException)和编译时异常。编译时异常就是 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。运行时异常就是Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。比如:NullPointerException(空指针错误)、ArrayIndexOutOfBoundsException(数组越界错误)等

JDK JVM JRE的关系区别

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

JDK是java开发工具包,[Java Development Kit],包括了Java运行环境(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API 包括rt.jar)。

      bin:最主要的是编译器(javac.exe)include:java和JVM交互用的头文件lib:类库jre:java运行环境

JRE(Java Runtime Environment,Java运行环境),包含JVM标准实现及Java核心类库。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)

  1. JDK 用于开发,JRE 用于运行java程序 ;如果只是运行Java程序,可以只安装JRE,无序安装JDK。
  2. JDk包含JRE,JDK 和 JRE 中都包含 JVM。
  3. JVM 是 java 编程语言的核心并且具有平台独立性。

Math类的取整函数

  • ceil的英文意义是天花板,该方法就表示向上取整,Math.ceil(11.3)的结果为12,Math.ceil(-11.6)的结果为-11;
  • floor的英文是地板,该方法就表示向下取整,Math.floor(11.6)的结果是11,Math.floor(-11.4)的结果-12;
  • 最难掌握的是round方法,他表示“四舍五入”,算法为Math.floor(x+0.5),即将原来的数字加上0.5后再向下取整,所以,Math.round(11.5)的结果是12,Math.round(-11.5)的结果为-11.

双亲委派机制

双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。

(1) 如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给

父类加载器去完成;

(2) 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终到达顶层的启动类加

载器;

(3) 如果父类加载器可以完成类加载任务,就成功返回;如果父加载器无法完成这个加载任务时,子加载

器才会尝试自己去加载。

总结来说:

(1)查看已加载的类,如果没有,就委派上级,在上级查找。

(2)如果在上级中都没有找到,就在自己类加载器中找

(3)如果在上级中找到,就在上级中进行加载

Java面试题总结 | Java基础部分2(持续更新)

优点:(1)避免类重复加载;(2)保护程序的安全,防止核心 API 被修改(沙箱安全机制);使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。

Ps: Java 虚拟机是如何判定两个 Java 类是相同的:

(1) 全名要相同

(2) 加载此类的类加载器要一样

只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码, 被不同的类加载器加载之后所得到的类,也是不同的。

双亲委派模型是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object 这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object 类,而且这些类之间是不兼容的。通过双亲委派模型,对于 Java 核心库的类的加载工作由启动类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

自定义加载器如何写

需要继承ClassLoader类或URLClassLoader,并至少重写其中的findClass(String name)方法,若想打破双亲委托机制,需要重写loadClass方法

主要加载:自己指定路径的class文件

抽象类

在java中并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。

由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。

父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。

抽象类和接口的区别

从设计目的上来说,二者有如下的区别:

**接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务;**对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。

抽象类体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。

从使用方式上来说,二者有如下的区别:

  • 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法。
  • 接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量。
  • 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
  • 接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
  • 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。

总结:接口体现的是一种规范,而抽象类体现的是一种模板设计,从使用方法来看,接口中不能包含普通方法、普通成员变量,构造方法和初始化块,但是在抽象类中都可以包含,其中抽象类的构造方法并不是用于创建对象,而是让其子类调用这些构造器来完成抽象类的初始化操作。一个java类只能继承一个父类,但是可以实现多个接口,这样可以弥补Java单继承的不足

扩展阅读

接口和抽象类很像,它们都具有如下共同的特征:

  • 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
  • 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

抽象类和接口的使用场景

抽象类的应用场景一般用于抽取不同事物的共有特性,然后可以用接口实现不同事物的不同行为。遇到需求,先分析不同的对象是否有共同点,比如门,都可以开和关,这个就可以抽取出来定义一个抽象类,而有的门有门铃可以摁,有的门没有,那就将有门铃的对象实现摁门铃这个动作的接口。

IO流

IO(Input Output)用于实现对数据的输入与输出操作,Java把不同的输入/输出源(键盘、文件、网络等)抽象表述为流(Stream)。流是从起源到接收的有序数据,有了它程序就可以采用同一方式访问不同的输入/输出源。

  • 按照数据流向,可以将流分为输入流和输出流,其中输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据。
  • 按照数据类型,可以将流分为字节流和字符流,其中字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。
  • 按照处理功能,可以将流分为节点流和处理流,其中节点流可以直接从/向一个特定的IO设备(磁盘、网络等)读/写数据,也称为低级流,而处理流是对节点流的连接或封装,用于简化数据读/写功能或提高效率,也称为高级流。

final和finally的区别

final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。 finally 是 Java 保证重点代码一定要被执行的一种机制。

泛型

Java 泛型(generics) 是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。

泛型类

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T> {private T key;public Generic(T key) {this.key = key;}public T getKey() {return key;}
}

泛型接口:

public interface Generator<T> {public T method();
}

泛型方法

public static <E> void printArray(E[] inputArray) {for (E element : inputArray) {System.out.printf("%s ", element);}System.out.println();
}

使用:

// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray(intArray);
printArray(stringArray);