JDK 即为 Java 开发工具包,包含编写 Java 程序所必须的编译、运行等开发工具以及 JRE。开发工具如:
JRE 即为 Java 运行环境,提供了运行 Java 应用程序所必须的软件环境,包含有 Java 虚拟机(JVM)和丰富的系统类库。系统类库即为 Java 提前封装好的功能类,只需拿来直接使用即可,可以大大的提高开发效率。
JVM 即为 Java 虚拟机,提供了字节码文件(.class)的运行环境支持。
这个问题,面试官可以衍生提问,Java 是编译执行的语言,还是解释执行的语言。 Java 中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在 Java 中,这种供虚拟机理解的代码叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java 源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了 Java 的编译与解释并存的特点。
=> 编译器 => JVM 可执行的 Java 字节码(即虚拟指令)=> JVM => JVM 中解释器 => 机器可执行的二进制机器码 => 程序运行
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
解释型语言:解释型语言,是在运行的时候将程序翻译成机器语言。解释型语言的程序不需要在运行前编译,在运行程序的时候才翻译,专门的解释器负责在每个语句执行的时候解释程序代码。这样解释型语言每执行一次就要翻译一次,效率比较低 例如:Python、PHP 。
面向对象是一种思想,万事万物抽象成一个对象,这里只讨论面向对象编程(OOP),Java 是一个支持并发、基于类和面向对象的计算机编程语言。
类是抽象的概念,是万事万物的抽象,是一类事物的共同特征的集合。 用计算机语言来描述 类 ,是 属性 和 方法 的集合。
对象是类的具象,是一个实体。 对于我们每个人这个个体,都是抽象概念人 类 的不同的 实体 。
封装,给对象提供了隐藏内部特性和行为的能力。对象提供一些能被其他对象访问的方法来改变它内部的数据。
在 Java 当中,有 4 种修饰符: default、public、private 和 protected 。每一种修饰符给其他的位于同一个包或者不同包下面对象赋予了不同的访问权限,权限如下:
访问权限 | 类 | 包 | 子类 | 其他包 |
---|---|---|---|---|
public | ∨ | ∨ | ∨ | ∨ |
protect | ∨ | ∨ | ∨ | × |
default | ∨ | ∨ | × | × |
private | ∨ | × | × | × |
继承,使对象基于基类字段和方法,新增自定义的的方法和属性。继承提供了代码的重用行,也可以在不修改类的情况下给现存的类添加新特性。
多态,程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
Java中有两种形式可以实现多态:
面向过程:就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,
面向对象:是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
整数型:默认 int 型,小数默认是 double 型。Float 和 Long 类型的必须加后缀。
比如:float f = 100f 。
引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。
引用类型指向一个对象,不是原始值,指向对象的变量是引用变量
在java里面除去基本数据类型的其他类型都是引用类型,自己定义的class类都是引用类型,可以像基本类型一样使用。
引用类型常见的有:String、StringBuffer、ArrayList、HashSet、HashMap等
特别注意,String 是引用类型不是基本类型。
引用类型 | 对象是否可引用 | 回收时间 | 使用场景 |
---|---|---|---|
强引用 | 可以 | 从不回收 | 普遍对象的状态 |
软引用 | 可以 | 内存不足时 | 内存敏感的高速缓存 |
弱引用 | 可以 | 下一次GC | 对象缓存 |
虚引用 | 不可以 | 下一次GC | 一般用于追踪垃圾收集器的回收动作 |
值传递,是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。
引用传递,一般是对于对象型变量而言的,传递的是该对象地址的一个副本,并不是原对象本身。
一般认为,Java 内的传递都是值传递,Java 中实例对象的传递是引用传递。
例如 String 类,两个引用所指向的 String 都是 "abc" ,但可能出现他们实际对应的对象并不是同一个(和 JVM 实现方式有关),因此用 == 判断他们可能不相等,但用 equals 方法判断一定是相等的。
从面向对象的角度来讲,我们知道所有的对象都是通过类来描绘的,但是反过来却不是这样,并不是 所有的类都是用来描绘对象的, 如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就可以认为是抽象类 。
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。
接口,在JAVA编程语言中是一个抽象类型,主要是 抽象方法的集合 ,接口中的变量定义必须为public static final类型。接口通常以interface来声明。
参数 | 抽象类 | 接口 |
---|---|---|
默认的方法实现 | 它可以有默认的方法实现 | 接口完全是抽象的。它根本不存在方法的实现 |
实现 | 子类使用 extends 关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。 | 子类使用关键字 implements 来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
与正常Java类的区别 | 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 | 接口是完全不同的类型 |
访问修饰符 | 抽象方法可以有 public 、 protected 和 default 这些修饰符 | 接口方法默认修饰符是 public 。你不可以使用其它修饰符。 |
main方法 | 抽象方法可以有main方法并且我们可以运行它 | 接口没有main方法,因此我们不能运行它。(java8以后接口可以有default和static方法,所以可以运行main方法) |
多继承 | 抽象方法可以继承一个类和实现多个接口 | 接口只可以继承一个或多个其它接口 |
速度 | 它比接口速度要快 | 接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。 |
添加新方法 | 如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码 | 如果你往接口中添加方法,那么你必须改变实现该接口的类。 |
---->父类静态变量
--------->父类静态代码块
------------->子类静态变量、
----------------->子类静态代码块
--------------------->父类非静态变量(父类实例成员变量)
------------------------->父类构造函数
----------------------------->子类非静态变量(子类实例成员变量)
--------------------------------->子类构造函数
简单的说,就是在一个类、接口或者方法的内部创建另一个类。这样理解的话,创建内部类的方法就很明确了。当然,详细的可以看看 《Java 内部类总结(吐血之作)》 文章。
内部类可以很好的实现隐藏(一般的非内部类,是不允许有 private 与protected权限的,但内部类可以)
内部类拥有外围类的所有元素的访问权限
可是实现多重继承
可以避免修改接口而实现同一个类中两种同名方法的调用。
可以继承其他类或实现其他接口,在 Java 集合的流式操作中,我们常常这么干。
一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。
如果你把静态嵌套类当作内部类的一种特例,那在这种情况下不可以访问外部类的普通成员变量,而只能访问外部类中的静态成员,例如:
class Outer { static int x; static class Inner { void test() { syso(x); } } } 复制代码
当新对象被创建的时候,构造方法会被调用。每一个类都有构造方法。在程序员没有给类提供构造方法的情况下,Java 编译器会为这个类创建一个默认的构造方法。
Java 中构造方法重载和方法重载很相似。可以为一个类创建多个构造方法。每一个构造方法必须有它自己唯一的参数列表。
父类的 equals ,一般情况下是无法满足子类的 equals 的需求。
比如所有的对象都继承 Object ,默认使用的是 Object 的 equals 方法,在比较两个对象的时候,是看他们是否指向同一个地址。但是我们的需求是对象的某个属性相同,就相等了,而默认的 equals 方法满足不了当前的需求,所以我们要重写 equals 方法。
如果重写了 equals 方法,就必须重写 hashCode 方法,否则就会降低 Map 等集合的索引速度。
理解答案与4.8.1差不多,
equals 方法,用于比较对象的内容是否相等。
当覆盖了 equals 方法时,比较对象是否相等将通过覆盖后的 equals 方法进行比较(判断对象的内容是否相等)。 hashCode 方法,大多在集合中用到。
将对象放入到集合中时,首先判断要放入对象的 hashCode 值与集合中的任意一个元素的 hashCode 值是否相等,如果不相等直接将该对象放入集合中。 如果 hashCode 值相等,然后再通过 equals 方法判断要放入对象与集合中的任意一个对象是否相等,如果 equals 判断不相等,直接将该元素放入到集合中,否则不放入。
可能会发生,这个被称为哈希碰撞。当然,相等的对象,即我们重写了 equals 方法,一定也要重写 hashCode 方法,否则将出现我们在 HashMap 中,相等的对象作为 key ,将找不到对应的 value 。
equals 不相等,hashCode 可能相等。
equals 相等,请重写 hashCode 方法,保证 hashCode 相等。
一般来说,hashCode 方法的重写,可以看看 《科普:为什么 String hashCode 方法选择数字31作为乘子》 方法。
String 类中使用 final 关键字字符数组保存字符串,代码:
private final char value[], 复制代码
所以string对象是不可变的。
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在 AbstractStringBuilder 中也是使用字符数组保存字符串 char[] value ,但是没有用 final 关键字修饰,代码:
char[]value 复制代码
这两种对象都是可变的。
AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
性能:每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。
StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
首先,在 String 池内找,找到 "xyz" 字符串,不创建 "xyz" 对应的 String 对象,否则创建一个对象。 然后,遇到 new 关键字,在内存上创建 String 对象,并将其返回给 s ,又一个对象。 所以,总共是 1 个或者 2 个对象
StringTokenizer ,是一个用来分割字符串的工具类。
示例代码如下:
StringTokenizer st = new StringTokenizer(”Hello World”); while (st.hasMoreTokens()) { System.out.println(st.nextToken()); } 复制代码
输出如下:
Hello World
自动装箱和拆箱,就是基本类型和引用类型之间的转换。
如果你在 Java5 下进行过编程的话,你一定不会陌生这一点,你不能直接地向集合( Collection )中放入原始类型值,因为集合只接收对象。
通常这种情况下你的做法是,将这些原始类型的值转换成对象,然后将这些转换的对象放入集合中。使用 Integer、Double、Boolean 等这些类,我们可以将原始类型值转换成对应的对象,但是从某些程度可能使得代码不是那么简洁精炼。
为了让代码简练,Java5 引入了具有在原始类型和对象类型自动转换的装箱和拆箱机制。 但是自动装箱和拆箱并非完美,在使用时需要有一些注意事项,如果没有搞明白自动装箱和拆箱,可能会引起难以察觉的 Bug 。
int 是基本数据类型。
Integer 是其包装类,注意是一个类。
需要注意下 Integer 的缓存策略
final ,是修饰符关键字。
Class 类:如果一个类被声明为 final ,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract 的,又被声明为 final 的。
变量或方法声明为 final ,可以保证它们在使用中不被改变。被声明为 final 的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。被声明为 final 的方法也同样只能使用,不能重写。
另外,在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的private 方法都隐式地指定为 final 。
在异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。
在以下 4 种特殊情况下,finally块不会被执行:
在 finally 语句块中发生了异常。
在前面的代码中用了 System.exit() 退出程序。
程序所在的线程死亡。
关闭 CPU 。
finalize ,是方法名。
Java 允许使用 #finalize() 方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。
它是在 Object 类中定义的,因此所有的类都继承了它。 子类覆盖 finalize() 方法,以整理系统资源或者执行其他清理工作。
finalize() 方法,是在垃圾收集器删除对象之前对这个对象调用的。 一般情况下,我们在业务中不会自己实现这个方法,更多是在一些框架中使用。
不能,因为 String 是 final 修饰。
static 变量在 Java 中是属于类的,它在所有的实例中的值是一样的。当类被 Java 虚拟机载入的时候,会对 static 变量进行初始化。如果你的代码尝试不用实例来访问非 static 的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
如果你的代码尝试不用实例来访问非 static 的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。
生命周期的不同:
调用方式不同:
别名不同:
数据存储位置不同:
成员变量数据存储在堆内存的对象中,所以也叫对象的特有数据。
静态变量数据存储在方法区(共享数据区)的静态区,所以也叫对象的共享数据。
-->父类静态变量
----->父类静态代码块
--------->子类静态变量
------------->子类静态代码块
------------------>父类普通变量
----------------------->父类普通代码块
----------------------------->父类构造函数
------------------------------------>子类普通变量
------------------------------------------>子类普通代码块
------------------------------------------------->子类构造函数
transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程,
transient 只能修饰变量,不能修饰类和方法。
对于不想进行序列化的变量,使用 transient 关键字修饰,
volatile 关键字用在多线程同步中,可保证读取的可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的读取值,而非 cache 中
不能 , 多个线程对 volatile 的写操作,无法保证线程安全。例如假如线程 1,线程 2 在进行 read,load 操作中,发现主内存中 count 的值都是 5,那么都会加载这个最新的值,在线程 1 堆 count 进行修改之后,会 write 到主内存中,主内存中的 count 变量就会变为 6;线程 2 由于已经进行 read,load 操作,在进行运算之后,也会更新主内存 count 的变量值为 6;导致两个线程及时用 volatile 关键字修改之后,还是会存在并发的情况
Java IO 相关的类,在 java.io 包下,具体操作分成面向字节(Byte)和面向字符(Character)两种方式。如下图所示:
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。
可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。 序列化是为了解决在对对象流进行读写操作时所引发的问题。 反序列化的过程,则是和序列化相反的过程。
我们不能将序列化局限在 Java 对象转换成二进制数组,比如,将一个 Java 对象转换成 JSON 字符串等,这也可以理解为是序列化。
将需要被序列化的类,实现 Serializable 接口,该接口没有需要实现的方法,implements Serializable 只是为了标注该对象是可被序列化的。
序列化
反序列化
实际场景下,我们使用的克隆比较少,更多是对象之间的属性克隆。例如说,将 DO 的属性复制到 DTO 中,又或者将 DTO 的属性复制到 VO 中。此时,我们一般使用 BeanUtils 工具类。
异常机制是指当程序出现错误后,程序如何处理。具体来说,异常机制提供了程序退出的安全通道。当出现错误后,程序执行的流程发生改变,程序的控制权转移到异常处理器。
程序错误分为三种:
Throwable 类图
Throwable有两个重要的子类 :
二者都是 Java 异常处理的重要子类,各自都包含大量子类
Error(错误),表示系统级的错误和程序不必处理的异常,是 Java 运行环境中的内部错误或者硬件问题。
Exception(异常),表示需要捕捉或者需要程序进行处理的异常,它处理的是因为程序设计的瑕疵而引起的问题或者在外的输入等引起的一般性问题,是程序必须处理的。Exception 又分为运行时异常,受检查异常。
Error:Error类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关 。比如:
Exception: 在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义 异常,比如:
ArrayIndexOutOfBoundsException(数组下标越界)
NullPointerException(空指针异常)
ArithmeticException(算术异常)
MissingResourceException(丢失资源)
ClassNotFoundException(找不到类)
BufferOverflowException
ClassCastException
Effective Java中对异常的使用给出了以下指导原则 :
不要将异常处理用于正常的控制流(设计良好的API不应该强迫它的调用者为了正常的控制流而使用异常) 对可以恢复的情况使用受检异常,对编程错误使用运行时异常 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生) 优先使用标准的异常 每个方法抛出的异常都要有文档 保持异常的原子性 不要在catch中忽略掉捕获到的异常
不管程序是否发生了异常, finally 语句块都会被执行,甚至当没有catch 声明但抛出了一个异常时, finally 语句块也会被执行。
finally 语句块通常用于释放资源, 如 I/O 缓冲区, 数据库连接等等。
UnsupportedOperationException ,是用于表明操作不支持的异常。
在 JDK 类中已被大量运用,在集合框架java.util.Collections.UnmodifiableCollection 将会在所有 add 和 remove 操作中抛出这个异常。
当程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。我们认为 Java 并不是动态语言,但是它却又一个非常突出的动态相关的机制
断言,在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。
一般来说,断言用于保证程序最基本、关键的正确性。断言检查通常在开发和测试时开启。为了保证程序的执行效率,在软件发布后断言检查通常是关闭的。 断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为true;如果表达式的值为 false ,那么系统会报告一个AssertionError 错误。断言的使用如下面的代码所示:
assert(a > 0); // throws an AssertionError if a <= 0 复制代码
断言可以有两种形式:
assert Expression1; 。 assert Expression1 : Expression2;
Expression1 应该总是产生一个布尔值。 Expression2 可以是得出一个值的任意表达式;这个值用于生成显示更多调试信息的字符串消息。
要在运行时启用断言,可以在启动 JVM 时使用 -enableassertions 或者 -ea 标记。要在运行时选择禁用断言,可以在启动 JVM 时使用 -da 或者 -disableassertions 标记。要在系统类中启用或禁用断言,可使用 -esa 或 -dsa 标记。还可以在包的基础上启用或者禁用断言。
也欢迎关注微信公众号【Ccww笔记】,原创技术文章第一时间推出
如果此文对你有帮助、喜欢的话,那就点个赞呗,点个关注呗!