Java基础

Java版本划分

  1. Java SE-标准版,主要用于普通PC、工作站的Java控制台或桌面程序的基础开发
  2. Java ME-微型版,用于移动设备、嵌入式设备上的Java应用程序开发。
  3. Java EE-企业版,用于开发、部署和管理企业级、可扩展的大型软件或Web应用。

J2SE、J2ME、J2EE与Java SE、Java ME、Java EE的关系?

答:Java5.0版本后,J2SE、J2EE、J2ME分别更名为Java SE、Java EE、Java ME,由于习惯的原因,我们依然称之为J2SE、J2EE、J2ME。

Java语言的特点

  1. 简单性:Java衍生于C/C++,但它略去了C/C++中学习起来比较难的多继承、指针等概念,并通过自动垃圾回收机制大大简化了程序员的内存管理工作,所以Java语言学习起来更简单,使用起来也更方便。
  2. 面向对象:Java是一种面向对象的编程语言。
  3. 分布式:Java是面向网络的语言,通过它提供的类库可以处理TCP/IP协议,用户可以通过URL地址在网络上很方便地访问其他对象。
  4. 健壮性:Java非常强调早期的问题检测、后期动态的检测,以及消除容易出错的情况,其采用的指针模型可以消除重写内存和损坏数据的可能性。
  5. 安全性:Java不支持指针,一切对内存的访问都必须通过对象实例来完成,有效的防止黑客使用欺骗手段访问对象的私有成员,同时也避免由于指针操作导致程序或系统崩溃。
  6. 可移植性:Java并不依赖平台,用Java编写的程序可以移植到不同的操作系统上。
  7. 解释型:Java解释器可以在任何移植了解释器的机器上直接执行Java字节码。
  8. 高性能:字节码可以(在运行时)动态地转换成对应运行这个应用地特定CPU地机器码。(即时编译器可以监控哪些代码频繁执行,并优化这些代码提高速度)
  9. 多线程:Java支持并发程序设计,它可以同时执行多个程序,能处理不同任务。
  10. 动态性:库中可以自由地添加新方法和实例变量,而对客户端却没有任何影响。

JDK、JRE、JVM的区别

JDK( Java Development Kit ),Java开发工具包,提供了编译、运行Java程序所需要的各种工具 ,除了包含JRE以外还包含了开发Java程序所必须的命令工具。

JRE( Java Runtime Environment),Java运行环境,主要包含两个部分:JVM和Java系统类库。所有的Java 程序都要在JRE下才能运行。普通用户需要运行已开发好的Java程序,只需要安装JRE即可。

JVM( Java Virtual Mechinal ),Java虚拟机,负责加载、执行字节码文件(.class),每种类型的操作系统都有一种对应的JVM,JVM屏蔽了底层操作系统的差异,使Java程序能够做到”一次编写,多处运行“。

JDK=JRE+编译、运行等命令工具

JRE=JVM+Java系统类库

switch语句

switch-case语句中,case的标签可以是:

  • 类型为char、byte、short或int的常量表达式(能自动转为int的类型,否则必须进行强制类型转换)
  • 枚举常量
  • 从Java7开始,case标签还可以是字符串字面量

重载和重写

重载:同一个方法对于输入的数据不同,做出不同的响应

  • 方法名相同
  • 形参不同:数据类型不同或参数顺序不同
  • 访问修饰符、返回类型不做要求

重写:当子类继承父类的方法需,当输入的数据相同需要做出不同于父类的响应时,覆盖父类的方法

  • 方法名相同、参数相同
  • 抛出的异常小于等于父类抛出的异常、返回的类型与父类返回类型相同或为其子类(void、基本类型不能被修改)
  • 访问修饰符大于等于父类

静态方法、构造方法不能被重写,重载为编译期、重写为运行期

String中compareTo()与equals()的区别

int compareTo(String other):按照字典顺序,如果字符串位于other之前,返回一个负数;如果字符串位于other之后,返回一个正数;如果两个字符串相等,返回0

boolean equals(Object other):如果字符串与other相等,返回true

Comparator和Comparable的区别

comparable接口出自java.lang包,它有一个 compareTo(Object obj)方法用来排序

comparator接口出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序

Comparator和Comparable一般都用于实现排序,如:Collections.sort(List list, Comparator<? super T> c) 方法可以通过传入Comparator实现类来自定义排序。

基本类型和包装类的区别

  • 成员变量为基本类型会有默认值不为null,包装类型默认默认值为null
  • 包装类型可用于泛型,基本类型不能用于泛型
  • 基本数据类型占用空间更小
  • 基本类型的局部变量存在于虚拟机栈中,基本类型的成员变量存在于堆中,包装类型属于对象类型,基本存在于堆中

在Java 5中,引入了自动装箱( 调用包装类的valueOf() )和自动拆箱( 调用xxxValue() )功能, Java可以根据上下文,自动进行转换,极大地简化了相关编程。

在Java 5中新增了静态工厂方法valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进:Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。

面向对象与面向过程的区别

主要为解决问题的方式不同

  • 面向过程会将解决问题的过程拆成一个个方法,通过执行一个个方法解决问题
  • 面向对象会抽象出对象,通过对象执行方法的方式解决问题

面向对象三大特征

  1. 封装:把一个对象的属性隐藏在对象内部,不允许外部对象直接访问对象的属性,但是可以提供一些可以被外界访问的方法来操作属性。

  2. 继承:使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。

  3. 多态:一个对象具有多种状态,具体表现为父类指向子类实例

通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。子类拥有父类所有属性和方法(包括私有),但是父类中的私有属性和方法不能访问。

接口和抽象类的区别

相同点:

  • 不能被实例化
  • 可以包含抽象方法
  • 可以有默认的实现方法

不同点:

  • 接口主要用于对类的行为进行约束,让实现类具有特定的行为(方法)。抽象类主要用于代码的复用,定义类之间的关系
  • 一个类可以实现多个接口,但只能继承一个类
  • 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法
  • 接口中的成员变量为public static final 类型,不能被修改且必须有初始值。抽象类的成员变量可以为各种类型

接口中的抽象方法为public abstract、抽象类中抽象方法为 public 或 protected

浅拷贝和深拷贝

浅拷贝:在堆中创建一个新的对象,当原对象内部的属性为引用类型时,浅拷贝直接复制其引用地址,拷贝对象和原对象的内部属性指向同一个内部对象。

深拷贝:深拷贝会复制整个对象包括内部引用类型属性所引用的对象。

Object中常见的方法

  1. equals 比较两个对象内存地址是否相等
  2. hashCode 返回对象的hash码(int整数)
  3. toString 返回类名字的哈希码的16进制字符串
  4. wait 暂停线程
  5. notify 唤醒在一个在该对象中暂停的线程
  6. notifyAll 唤醒在该对象中暂停的所有线程
  7. getClass 获取该类的类对象
  8. finalize 垃圾回收时执行的方法

为什么重写equals()方法必须要重写hashCode()方法?

两个对象相等则其hash值相等,通过equals方法判断两个对象相等则其hashCode值相等。当重写equals不重写hashCode时,可能会出现equals判断两个对象相等,但其hash值不相等。

String为什么是不可变的?

  1. 保存字符串的数组被final修饰且为私有的,String类并没有提供修改这个字符串数组中值的方法
  2. String被final修饰不能被继承避免了子类破坏String的不可变。

String、StringBuffer、StringBuilder的区别

String是Java语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的Immutable类,被声明成为final class,所有属性也都是final的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的String对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

StringBufer是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用append或者add方法,把字符串添加到已有序列的末尾或者指定位置。 StringBufer本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是StringBuilder。

StringBuilder是Java 1.5中新增的,在能力上和StringBufer没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

为了实现修改字符序列的目的, StringBufer和StringBuilder底层都是利用可修改的(char, JDK 9以后是byte)数组,二者都继承了AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了synchronized。

构建时初始字符串长度为16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是16)

泛型

编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型,使用泛型后从容器中取出对象编译器自动转换对象类型。

对Java平台的理解

  1. Java语言本身、JDK中提供的核心类库和相关工具
  • 面向对象(封装、继承、多态)
  • 跨平台(一次书写到处运行,跨平台能力)
  • 语言(泛型、Lambda)
  • 类库(集合、网络、并发、IO/NIO)
  • 工具(基本的编译工具、虚拟机性能诊断工具)
  1. Java虚拟机
  • 垃圾收集(GC),Java通过垃圾收集器回收分配内存,程序员不需要自己操心内存的分配和回收
  • 对于虚拟机而言,只要是符合规范的字节码,它们都能被加载执行,当然,能正常运行的程序光满足这点是不行的,程序本身需要保证在运行时不出现异常。所以, Scala、 Kotlin、 Jython等语言也可以跑在虚拟机上。

Java是解释执行的吗?

Java是解释执行这句话不太准确,首先Java源代码通过Javac编译为字节码,通过 Java虚拟机(JVM)内嵌的解释器将字节码转换成为最终的机器码。但是常见的JVM都提供了JIT编译器(动态编译器),JIT能过在运行时将热点代码编译成机器码,这种情况下这部分热点代码属于编译执行,而不是解释执行。Java 9提供的AOT编译器可以直接将所有代码编译成机器码执行。

Java采用的是解释和编译混合的模式。它首先通过javac将源码编译成字节码文件class.然后在运行的时候通过解释器或者JIT将字节码转换成最终的机器码。
只是用解释器的缺点:抛弃了JIT可能带来的性能优势。如果代码没有被JIT编译的话,再次运行时需要重复解析。
只用JIT的缺点:
需要将全部的代码编译成本地机器码。要花更多的时间, JVM启动会变慢非常多;
增加可执行代码的长度(字节码比JIT编译后的机器码小很多),这将导致页面调度,从而降低程序的速度。
有些JIT编译器的优化方式,比如分支预测,如果不进行profling,往往并不能进行有效优化。
因此, HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。 JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。
JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。 JDK支持分层编译和AOT协作使用。
注: JIT为方法级,它会缓存编译过的字节码在CodeCache中,而不需要被重复解释

为什么不全部使用 AOT 呢?

这和 Java 语言的动态特性有千丝万缕的联系了。举个例子,CGLIB 动态代理使用的是 ASM 技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 .class 文件,如果全部使用 AOT 提前编译,也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用 JIT 即时编译器。

对于一次编译,到处运行的理解

一次编译,到处运行说的是Java语言跨平台的特性,程序从源代码到运行经过三个阶段:编码-编译-运行,Java在编译阶段体现了跨平台的特点,编译的大概过程:首先将Java源代码通过Javac编译为.CLASS文件字节码,该字节码文件即为可到处运行的文件,然后字节码会被转变为机器码,这是由JVM来执行的,即Java的第二次编译。

到处运行的关键和前提就是JVM,因为在第二次编译中JVM起着关键作用。在可以运行Java虚拟机的地方都内含着一个JVM操作系统,从而使Java提供了各种不同平台上的虚拟机制、屏蔽不同操作系统之间的差异,因此实现了“到处运行”的效果 。

其实Java语言本身与其他的编程语言没有特别大的差异,并不是说Java语言可以跨平台,而是在不同的平台都有可以让Java语言运行的环境而已,所以才有了Java一次编译,到处运行这样的效果。

Exception和Error的区别

Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。Exception和Error体现了Java平台设计者对不同异常情况的分类。

  • Error:系统错误类,是指在正常情况下,不大可能出现的情况,代表程序运行时Java系统内部错误,一般由硬件或操作系统引发。既然是非正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError之类,都是Error的子类。

  • Exception:异常类,是程序正常运行中,可以预料的意外情况,应该被捕获并进行相应处理。它包含子类RuntimeException、IOException异常,RuntimeException类表示Java程序运行时产生的异常,如数组下标越界、对象类型强制转换错误、空指针等,IOException类及其子类表示各种I/O错误。

异常又分为检查(checked)异常和非检查(unchecked)异常,检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。Error和RuntimeException类及其子类为非检查异常,IOException类及其子类为检查异常。

非检查异常就是所谓的运行时异常,类似 NullPointerException、 ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。

image-20230101181148657

NoClassDefFoundError和ClassNotFoundException的区别

ClassNotFoundException的产生原因主要是:

  • Java支持使用反射方式在运行时动态加载类,例如使用Class.forName方法来动态地加载类时,可以将类名作为参数传递给上述方法从而将指定类加载到JVM内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。
    解决该问题需要确保所需的类连同它依赖的包存在于类路径中,常见问题在于类名书写错误。

  • 另外还有一个导致ClassNotFoundException的原因就是:当一个类已经某个类加载器加载到内存中了,此时另一个类加载器又尝试着动态地从同一个包中加载这个类。通过控制动态类加载过程,可以避免上述情况发生。

NoClassDefFoundError产生的原因在于:

  • 如果JVM或者ClassLoader实例尝试加载(可以通过正常的方法调用,也可能是使用new来创建新的对象)类的时候却找不到类的定义。要查找的类在编译的时候是存在的,运行的时候却找不到了。这个时候就会导致NoClassDefFoundError。造成该问题的原因可能是打包过程漏掉了部分类,或者jar包出现损坏或者篡改。解决这个问题的办法是查找那些在开发期间存在于类路径下但在运行期间却不在类路径下的类。

final、 finally、 finalize的不同

final可以用来修饰类、方法、变量,分别有不同的意义, final修饰的class代表不可以继承扩展, final的变量是不可以修改的,而final的方法也是不可以重写的。
finally则是Java保证重点代码一定要被执行的一种机制。我们可以使用try-finally或者try-catch-finally来进行类似关闭JDBC连接、保证unlock锁等动作。
finalize是基础类java.lang.Object的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。 fnalize机制现在已经不推荐使用,并且在JDK 9开始被标记为deprecated

为什么不推荐使用finalize?简单说,你无法保证fnalize什么时候执行,执行的是否符合预期。使用不当会影响性能,导致程序死锁、挂起等。

finally不执行的情况:1.system.exit(1)退出 2.无限循环 3.线程被杀死

强引用、软引用、弱引用、幻象引用的区别

在Java语言中,除了基本数据类型外,其他的都是指向各类对象的对象引用; Java中根据其生命周期的长短,将引用分为4类,不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。

  1. 强引用

特点:我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。 当JVM内存空间不足, JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。

  1. 软引用

特点:软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收, Java虚拟机就会把这个软引用加入到与之关联的引用
队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。
应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

  1. 弱引用

特点:弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收, Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
应用场景:弱应用同样可用于内存敏感的缓存。

  1. 虚引用

特点:虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 fnalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

1
2
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。
应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。

反射和动态代理

  1. 关于反射

反射机制是Java语言提供的一种基础功能,赋予程序在运行时自省的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。反射技术常用在各类通用框架开发中。因为为了保证框架的通用性,需要根据配置文件加载不同的对象或类,并调用不同的方法,这个时候就会用到反射——运行时动态加载需要加载的对象。

  1. 动态代理

动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装RPC调用、面向切面的编程(AOP)。实现动态代理的方式很多,比如JDK自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似ASM、 cglib(基于ASM)、 Javassist等


Java基础
https://pursuemilk.github.io/2023/01/23/Java基础/
作者
PursueMilk
发布于
2023年1月23日
许可协议