Java 平台无关性
主要通过三个方面实现.
Java 语言规范: 通过规定 Java 语言中基本数据类型的取值范围和行为,比如 int 长度为 4 字节,这是固定的。
Class 文件: 所有 Java 文件要通过 javac 或者其他一些 java 编译器编译成统一的 Class 文件
Java 虚拟机:
通过 Java 虚拟机将字节码文件 (.Class) 转成对应平台的二进制文件
JVM 是平台相关的,需要在不同操作系统安装对应的虚拟机
程序运行过程
编译:java 源文件编译为 class 字节码文件
类加载:类加载器把字节码加载到虚拟机的方法区。
运行时创建对象
方法调用,JVM 执行引擎解释为机器码
CPU 执行指令
多线程切换上下文
Java 三大特性
封装:将一系列的操作和数据组合在一个包中,使用者调用这个包的时候不必了解包中具体的方法是如何实现的。
多态:父类的变量可以引用一个子类的对象,在运行时通过动态绑定来决定调用方法。
作用:
应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。// 继承
派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。
继承:一个类可以扩展出一个子类,子类可以继承父类的属性和方法,也可以添加自己的成员变量和方法。接口可以多继承,普通类只能单继承。
重载和重写
重写:子类具有和父类方法名和参数列表都相同的方法,返回值要不大于父类方法的返回值,抛出的异常要不大于父类抛出的异常,方法修饰符可见性要不小于父类。运行时多态。
是运行时多态,因为程序运行时,会从调用方法的类中根据继承关系逐级往上寻找该方法,这是在运行时才能进行的。
重载:同一个类中具有方法名相同但参数列表不同的方法,返回值不做要求。编译时多态。
Integer 和 int 区别
Integer 是 int 的包装类,所表示的变量是一个对象;而 int 所表示的变量是基本数据类型
自动装箱 (valueOf) 指的是将基本数据类型包装为一个包装类对象,自动拆箱 (intValue) 指的是将一个包装类对象转换为一个基本数据类型。
包装类的比较使用 equals,是对象间的比较
基本数据类型
byte 1 字节;short 2 字节
int, float 4 字节
long, double 8 字节
boolean 单独出现时 4 字节,数组时单个元素 1 字节
char 英文都是 1 字节,GBK 中文 2 字节,UTF-8 中文 3 字节
值传递和引用传递
值传递对基本数据类型而言的,传递的是变量值的一个副本,改变副本不影响原变量的值
引用传递对于对象型变量而言,传递的是对象地址的副本,不是原变量本身,所以对引用对象的操作会改变原变量的值。
== 和 equals 区别
== 比较的对象如果是基本数据类型,就是两者的值进行比较;如果是引用对象的比较,是判断对象的地址值是否相同
equals 如果比较的是 String 对象,就是判断字符串的值是否相同;如果比较的是 Object 对象,比较的是引用的地址内存;可以通过重写 equals 方法来自定义比较规则,也需要同时重写 hashCode 方法
方法修饰符可见类型
public: 对本包和不同包都是可见的
protected: 对不同包不可见
default: 只对本包中子类和本类可见
private:只对本类可见
Object 类,equals 和 hashCode
Object 的类是所有类的父类。equals,hashCode,toString 方法
equals 用来比较对象地址值是否相同
hashCode 返回由对象地址计算得出的一个哈希值
两者要同时重写的原因
使用 hashcode 方法提前校验,通过 hasCode 比较比较快,可以避免每一次比对都调用 equals 方法,提高效率
保证是同一个对象,如果重写了 equals 方法,而没有重写 hashCode 方法,会出现 equals 比较时相等的对象,hashCode 不相等的情况,重写 hashcode 方法就是为了避免这种情况的出现。
哈希值相同的对象 equals 比较不一定相等,存在两个对象计算得到 hashCode 相等的情况,这是哈希冲突。
避免哈希冲突?
哈希表的特点:关键字在表中位置和它之间存在一种确定的关系。
解决哈希冲突:
开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
线性探测再散列:放入元素,如果发生冲突,就往后找没有元素的位置;
平方探测再散列:如果发生冲突,放到 (冲突 + 1 平方) 的位置,如果还发生冲突,就放到 (冲突 - 1 平方) 的位置;如果还有人就放到 (冲突 + 2 平方) 的位置,以此类推,要是负数就倒序数。
随机探测再散列
链地址法:如果发生冲突,就继续往前一个元素上链接,形成一个链表,Java 的 hashmap 就是这种方法。
再哈希:用另一个方法再次进行一个哈希运算
建立一个公共溢出区:将哈希表分为基本表和溢出表两部分,范式和基本表发生冲突的元素,一律填入溢出表。
深拷贝,浅拷贝
clone:clone 方法声明为 protected,类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为 public。如果一个对象的类没有实现 Cloneable 接口,该对象调用 clone 方法会抛出一个 CloneNotSupport 异常。默认的 clone 方法是浅拷贝,一般重写 clone 方法需要实现 Cloneable 接口并指定访问修饰符为 public。
浅拷贝:重新在堆中创建内存,将对象进行拷贝,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因为共享同一块内存,会相互影响。(被浅拷贝的对象是会重新生成一个新的对象,新的对象和原来的对象是没有任何关系的,)如果对象中的某个属性是引用类型的话,那么该属性对应的对象是不会重新生成的,浅拷贝只会重新当前拷贝的对象,并不会重新生成其属性引用的对象。
深拷贝:从堆内存中开辟一个新的区域存放新对象,会把拷贝的对象和其属性引用的对象都重新生成新的对象。
实现:对拷贝的对象中所引用的数据类型再进行以拷贝;使用序列化
内部类
使用内部类主要有两个原因:内部类可以对同一个包中的其他类隐藏。内部类方法可以访问定义这个内部类的作用域中的数据,包括原本私有的数据。内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用美元符号 $ 分隔外部类名与内部类名,而虚拟机对此一无所知。
静态内部类:由 static 修饰,属于外部类本身,只加载一次。类可以定义的成分静态内部类都可以定义,可以访问外部类的静态变量和方法,通过 new 外部类.内部类构造器 来创建对象。只要内部类不需要访问外部类对象,就应该使用静态内部类。
成员内部类:属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可以访问外部类的所有内容,通过 new 外部类构造器.new 内部类构造器 来创建对象。
局部内部类:定义在方法、构造器、代码块、循环中。不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明这个局部类的代码块中。
匿名内部类:没有名字的局部内部类,可以简化代码,匿名内部类会立即创建一个匿名内部类的对象返回,对象类型相当于当前 new 的类的子类类型。匿名内部类一般用于实现事件监听器和其他回调。
类初始化的顺序
父类静态变量和静态代码块
子类静态变量和静态代码块
父类普通变量和代码块
父类构造器
子类普通变量和代码块
子类构造器
Comparable 和 Comparator 的区别
Comparable 和 Comparator 都是用来实现集合中元素的比较、排序的。
Comparable 是在集合内部定义的方法实现的排序,位于 java.util 下;Comparator 是在集合外部实现的排序,位于 java.lang 下。
Comparable 是一个对象本身就已经支持自比较所需要实现的接口,如 String、Integer 自己就实现了 Comparable 接口,可完成比较大小操作。
Comparator 是一个专用的比较器,当这个对象不支持自比较或者自比较函数不能满足要求时,可写一个比较器来完成两个对象之间大小的比较。Comparator 体现了一种策略模式 (strategy design pattern),就是不改变对象自身,而用一个策略对象 (strategy object) 来改变它的行为。
Comparable 是自己完成比较,Comparator 是通过外部重写比较规则实现比较。
String,StringBuilder,StringBuffer
String
一旦被创建就不可被修改,所以修改 String 变量值的时候是新建了一个 String 对象,赋值给原变量引用
两种创建方法
直接赋值一个字符串,就是将字符串放进常量池,位于栈中的变量直接引用常量池中的字符串。
new 方式创建先在堆中创建 String 对象,再去常量池中查找是否有赋值的字符串常量,找到了就直接使用,没找到就开辟空间存字符串。通过变量引用对象,对象引用字符串的形式创建。
StringBuilder & StringBuffer
都继承自 AbstractStringBuilder 类,是可变类(这是加分项)
前者线程不安全,后者通过 synchronized 锁保证线程安全
因此 StringBuilder 执行效率高,StringBuffer 执行效率低
final 关键字
所修饰的变量,是基本数据类型则值不能改变,访问时会被当做一个常量;是引用型变量的话,初始化后就不能指向另一个对象了。而且一定要显示地初始化赋值。
所修饰的类,不能被继承,其中方法默认是 final 修饰
final 修饰的方法不可被重写,但可以被重载
static 关键字
修饰代码块,使这个代码块在 JVM 加载之处就开辟一块空间单独存放代码块内容,且只加载一次。执行得到的结果存储在方法区并被线程共享。静态类中的方法直接和这个类关联,而不是和这个对象关联。可以直接通过类名来使用方法。
修饰非局部的成员变量,加载方式和静态代码块一样。由于在 JVM 内存中共享,会引起线程安全问题。解决:加 final;使用同步(volatile 关键字)。
修饰方法,通过类名调用。静态方法不可以直接调用其他成员方法、成员变量。
抽象类和接口
接口只能用 public * 和 *abstract * 修饰
* 区别分为四个方面:
成员变量:接口中默认 public static final
成员方法:java8 之前接口中默认是 public,java8 加入了 static 和 default,java9 中加入了 private,方法不能用 final 修饰,因为需要实现类重写;抽象类无限制
构造器:接口和抽象类都不能被实例化,但接口中没有构造器,抽象类中有
继承:接口可以多继承,抽象类只能单继承
抽象类和接口的选择?
如果知道某个类应该成为基类,那么第一选择应该是让它成为一个接口,只有在必须要有方法定义和成员变量的时候,才应该选择抽象类。在接口和抽象类的选择上,必须遵守这样一个原则:行为模型应该总是通过接口而不是抽象类定义。通过抽象类建立行为模型会出现的问题:如果有一个产品类 A,有两个子类 B 和 C 分别有自己的功能,如果出现一个既有 B 产品功能又有 C 产品功能的新产品需求,由于 Java 不允许多继承就出现了问题,而如果是接口的话只需要同时实现两个接口即可。
异常
所有的异常都继承自 Throwable 类的,分为 Error 和 Exception。
Error 类描述了 Java 运行时系统的内部错误和资源耗尽错误,如果出现了这种错误,一般无能为力。
Error 和 RuntimeException 的异常属于非检查型异常,其他的都是检查型异常。
常见的 RuntimeException 异常:
ClassCastException,错误的强制类型转换。
ArrayIndexOutOfBoundsException,数组访问越界。
NullPointerException,空指针异常。
常见的检查型异常:
FileNotFoundException,试图打开不存在的文件。
ClassNotFoundException,试图根据指定字符串查找 Class 对象,而这个类并不存在。
IOException,试图超越文件末尾继续读取数据。
异常处理:
抛出异常:遇到异常不进行具体处理,而是将异常抛出给调用者,由调用者根据情况处理。抛出异常有 2 种形式,一种是 throws 关键字声明抛出的异常,作用在方法上,一种是使用 throw 语句直接抛出异常,作用在方法内。
捕获异常:使用 try/catch 进行异常的捕获,try 中发生的异常会被 catch 代码块捕获,根据情况进行处理,如果有 finally 代码块无论是否发生异常都会执行,一般用于释放资源,Java 7 开始可以将资源定义在 try 代码块中自动释放资源。
try-catch-finally
finally 对 try 块中打开的物理资源进行回收 (JVM 垃圾回收机制回收对象占用的内存)。
这个回收如果放在 catch 中执行,不发生异常则不会被执行;放在 try 中,如发生异常前就被回收,那么 catch 就不会被执行。
java7 可以在 try () 圆括号中初始化或声明资源,会自动回收。但资源需要实现 AutoCloseable 接口
序列化
Java 对象在 JVM 运行时被创建,JVM 退出时存活对象被销毁。为了保证对象及其状态的持久化,就需要使用序列化了。序列化就是将对象通过 ObjectOutputStream 保存为字节流;反序列化就是将字节流还原为对象。
要实现 Serializable 接口来进行序列化。
序列化和反序列化必须保持序列化 ID 的一致。
静态、transient 修饰的变量和方法不能被序列化。
实现 Externalizable 可以自行决定哪些属性可以被序列化
反射
在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象方法的功能就是 Java 的反射机制。优点是运行时动态获取类的全部信息,缺点是破坏了类的封装性,泛型的约束性。
Class 类保存对象运行时信息,可以通过①类名.class ②对象名.getClass ()③Class.forName (类的全限定名) 方式获取 Class 实例
Class 类中的 getFields () 返回这个类支持的公共字段;getMethods () 返回公共方法;getCosntructors () 返回构造器数组(包括父类公共成员)
xxxDeclaredxxx () 可以返回全部字段、方法和构造器的数组(不包括父类的成员)
注解
可以给类、接口或者方法、变量添加一些额外信息;帮助编译器和 JVM 完成一些特定功能。
元注解:我们可以自定义一个注解,这时就需要在自定义注解中使用元注解来标识一些信息
@Target:约束注解作用位置:METHOD,VARIABLE,TYPE,PARAMETER,CONSTRUCTORS,LOACL_VARIABLE
@Rentention:约束注解作用的生命周期:SOURCE 源码,CLASS 字节码,RUNTIME 运行时
@Documented:表明这个注解应该被 javadoc 工具记录
@Inherited:表面某个被标注的类型是被继承
泛型
泛型的提出是为了编写重用性更好的代码。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
那么 Java 之所以引入它我认为主要有三个作用
类型检查,它将运行时类型转换的 ClassCastException 通过泛型提前到编译时期。
避免类型强转。
泛型可以泛型算法,增加代码的复用性。
实现
泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如 List 在运行时仅用一个 List 来表示。这样做的目的,是确保能和 Java 5 之前的版本开发二进制类库进行兼容。
Java 的泛型是如何工作的?什么是类型擦除?如何工作?
1、类型检查:在生成字节码之前提供类型检查
2、类型擦除:所有类型参数都用他们的限定类型替换,包括类、变量和方法(类型擦除)
3、如果类型擦除和多态性发生了冲突时,则在子类中生成桥方法解决
4、如果调用泛型方法的返回类型被擦除,则在调用该方法时插入强制类型转换
集合
数组和集合区别
区别 1:
数组既可以存储基本数据类型,又可以存储引用数据类型,基本数据类型存储的是值,引用数据类型存储的是地址值;
集合只能存储引用数据类型 (对象), 集合中也可以存储基本数据类型,但是在存储的时候会自动装箱 (JDK1.5 新特性) 变成对象.
区别 2:
数组长度是固定的,不能自动增长;
集合的长度的是可变的,可以根据元素的增加而增长.
使用情况:
1. 如果元素个数是固定的,推荐用数组
2. 如果元素个数不是固定的,推荐用集合
List
List 是一种线性列表结构,元素是有序、可重复的。
ArrayList 和 LinkedList 对比:
ArrayList 是实现了基于动态数组的数据结构,LinkedList 是基于链表结构。
对于随机访问的 get 和 set 方法查询元素,ArrayList 要优于 LinkedList,因为 LinkedList 循环链表寻找元素。
对于新增和删除操作 add 和 remove,LinkedList 比较高效,因为 ArrayList 要移动数据。
优缺点:
对 ArrayList 和 LinkedList 而言,在末尾增加一个元素所花的开销都是固定的。对 ArrayList 而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对 LinkedList 而言,这个开销是统一的,分配一个内部 Entry 对象。
在 ArrayList 集合中添加或者删除一个元素时,当前的列表移动元素后面所有的元素都会被移动。而 LinkedList 集合中添加或者删除一个元素的开销是固定的。
ArrayList 的空间浪费主要体现在在 list 列表的结尾预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗相当的空间
ArrayList 和 LinkedList 都不是线程安全的。
应用场景:
ArrayList 使用在查询比较多,但是插入和删除比较少的情况,而 LinkedList 用在查询比较少而插入删除比较多的情况
ArrayList
底层由数组实现,随机访问(RandomAccess 接口),读快写慢,由于写操作涉及元素的移动,因此写操作效率低。
三个成员变量:
elementData 是 ArrayList 的数据域,会预留一些容量保证性能,transient 修饰,不能被序列化;
size 表示 list 的实际大小,private;
modCount 继承自 AbstractList,记录了 ArrayList 添加或者删除元素这种结构性变化的次数。protected transient 修饰。
LinkedList
底层由链表实现,需要顺序访问元素,即使有索引也需要从头遍历,所以说写快读慢。
LinkedList 实现了 Deque 接口,具有队列的属性,可在尾部增加元素,在头部获取元素,也能操作头尾之间任意元素。
成员变量,序列化原理类似 ArrayList。
Vector 和 Stack
Vector 的实现和 ArrayList 基本一致,底层使用的也是数组,区别主要在于:
(1)Vector 的所有公有方法都使用了 synchronized 修饰保证线程安全性。
(2)增长策略不同,Vector 多了一个成员变量 capacityIncrement 用于标明扩容的增量。
Stack 是 Vector 的子类,实现和 Vector 基本一致,与之相比多提供了一些方法表达栈的含义比如 pop (),top ()。
HashSet
HashSet 中的元素是无序、不重复的,最多只能有一个 null 值。
HashSet 的底层是通过 HashMap 实现的,HashMap 的 key 值即 HashSet 存储的元素,所有 key 都使用相同的 value —— 一个 static final 修饰的变量名为 PRESENT 的 Object 类型的对象。所有关于 Set 的操作都是直接调用 HashMap 的方法实现的。
HashMap 是线程不安全的,因此 HashSet 也是线程不安全的。
去重:基本数据类型直接比较值;引用数据类型通过比较 hashCode 和 equal 方法
HashTable
HashTable 继承自 Dictionary 类
底层是数组 + 链表,key 和 value 都不能为 null。因为添加数据 put 操作使用了 synchronized 同步锁,实现了线程安全。
初始容量是 11,扩容方式是 oldSize * 2 + 1
计算索引的方式:哈希值 % table 数组长度,取模运算计算消耗较大
相比而言,HashMap 继承自 AbstractMap 类;JDK1.8 开始底层是数组 + 链表 / 红黑树,key、value 都可以为 null,线程不安全;初始容量 16,扩容 oldSize * 2;计算数据储存索引的方式:哈希值和数组长度减一进行与运算。
TreeMap
TreeMap 是基于红黑树的一种提供顺序访问的 Map,与 HashMap 不同的是它的 get、put、remove 之类操作都是 o (log (n)) 的时间复杂度,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断
HashMap
HashMap 继承自 AbstractMap,实现了 Map, Cloneable, Serializable 接口。
HashMap 默认初始化容量为 16,扩容是 oldSize * 2;扩容容量必须是 2 的幂次方、最大容量为 2 的 30 次方 、默认加载因子为 0.75。
工作原理
HashMap 在 Map.Entry 静态内部类实现中存储键值对。HashMap 使用哈希算法,在 put 和 get 方法中,它使用 hashCode () 和 equals () 方法。
当我们通过传递键值对调用 put 方法的时候,HashMap 使用 Key hashCode () 和哈希算法来找出存储键值对对的索引。Entry 存储在链表中,所以如果存在 entry,它使用 equals () 方法来检查传递的键是否已经存在,如果存在,它会覆盖原来的值,如果不存在,它会创建一个新的 entry 然后保存。当链表深度达到 8 的时候,会使用红黑树来存储数据。
当我们通过传递键调用 get 方法时,它再次使用 hashCode () 来找到数组中的索引,然后使用 equals () 方法找出正确的 Entry,然后返回它的值。
智一面热门岗位面试题:
高级java工程师