java 基础

Java语言有哪些特点

1,简单易学;2,面向对象(封装,继承,多态);3,平台无关性(Java虚拟机实现平台无关性);4,可靠性;5,安全性;6,支持多线程(C++语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而Java语言却提供了多线程支持);7,支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的,因此Java语言不仅支持网络编程而且很方便);8,编译与解释并存;

平台无关性
Java语言的一个非常重要的特点就是与平台的无关性,Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,需要编译成不同的目标代码。而Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

Java和C++的区别
都是面向对象的语言,都支持封装、继承和多态
Java 不提供指针来直接访问内存,程序内存更加安全
Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
Java 有自动内存管理机制,不需要程序员手动释放无用内存

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

面向过程
优点: 性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点: 没有面向对象易维护、易复用、易扩展

面向对象
优点: 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点: 性能比面向过程低

面向对象的三个特征
封装:提供了隐藏对象内部特性和行为的能力。将某事物的属性和行为包装到对象中,对象只对外提供与其它对象交互的必要接口
继承:给对象提供了从基类获取字段和方法的能力。继承提供了代码的重用行,也可以在不修改类的情况下给现存的类添加新特性。
多态:允许不同类的对象对同一消息作出响应。
虚拟机是如何实现多态的:动态绑定技术(dynamic binding),执行期间判断所引用对象的实际类型,根据实际类型调用对应的方法.

JVM、JDK 和 JRE 

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

Java程序运行过程

JDK 和 JRE
JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。
JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。
如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装JDK了。但有时您不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,要使用JSP部署Web应用程序,应用程序服务器会将JSP转换为 Java servlet,这就需要使用 JDK 来编译 servlet。

Oracle JDK 和 OpenJDK 的对比
1. Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一次;
2. OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的;
3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的问题,只需切换到Oracle JDK就可以解决问题。
4. 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能;
5. Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本;
6. Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。

接口和抽象类的区别
1.接口的方法默认是public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法
2.接口中的实例变量默认是final类型的,而抽象类中则不一定
3.一个类可以实现多个接口,但最多只能实现一个抽象类
4.一个类实现接口的话要实现接口的所有方法,而抽象类不一定
5.接口不能用new实例化
6.从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

Java中的方法覆盖(Overriding)和方法重载(Overloading)是什么意思?
方法重载:发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况。
方法覆盖:子类重新定义了父类的方法。方法覆盖必须有相同的方法名,参数列表和返回类型。覆盖者可能不会限制它所覆盖的方法的访问。

不可变对象
不可变对象指对象一旦被创建,状态就不能再改变。任何修改都会创建一个新的对象,如 String、Integer及其它包装类。

String intern()方法
String对象的intern()方法会首先从常量池中查找是否存在该常量值,如果常量池中不存在则现在常量池中创建,如果已经存在则直接返回.

自动装箱与拆箱
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;

int和Integer的区别
int是基本类型,直接存数值,而integer是对象,是int的包装类型,在拆箱和装箱中,二者自动转换.

Integer内部缓存
Integer类内部有一个staitic的Integer数组,存储的是一些已经完成初始化的Integer对象,一般值为(-128~127),若用==比较,则有时候会因为值不在缓存中而返回false,所以应该用equals比较。

值传递和引用传递
对象被值传递,意味着传递了对象的一个副本。因此,就算是改变了对象副本,也不会影响源对象的值。
对象被引用传递,意味着传递的并不是实际的对象,而是对象的引用。因此,外部对引用对象所做的改变会反映到所有的对象上。

String、StringBuffer和StringBuilder区别
1. String类中使用 final 关键字修饰字符数组,所以String对象是不可变的,每次对String类型进行操作都会产生一个新的String对象。所以尽量不要对string进行大量的拼接操作,否则会产生很多临时对象,导致GC开始工作,影响系统性能.
2. StringBuffer的内部实现是可变字符串,StringBuffer对方法加了同步锁,所以是线程安全的。
3. StringBuilder是jdk 1.5新增的,其功能和StringBuffer类似,但是非线程安全。因此在没有多线程问题的前提下,使用StringBuilder会取得更好的性能

集合框架 

1. List

  • Arraylist:数组(查询快,增删慢 线程不安全,效率高 )
  • Vector:数组(查询快,增删慢 线程安全,效率低 )
  • LinkedList:链表(查询慢,增删快 线程不安全,效率高 )

2. Set

  • HashSet(无序,唯一):哈希表或者叫散列集(hash table)
  • LinkedHashSet:链表和哈希表组成 。 由链表保证元素的排序 , 由哈希表证元素的唯一性
  • TreeSet(有序,唯一):红黑树(自平衡的排序二叉树。)

3. Map

  • HashMap:基于哈希表的Map接口实现(哈希表对键进行散列,Map结构即映射表存放键值对)
  • LinkedHashMap:HashMap 的基础上加上了链表数据结构
  • HashTable:哈希表
  • TreeMap:红黑树(自平衡的排序二叉树)

集合的选用
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。

Array和ArrayList的区别

  • Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
  • Array大小是固定的,ArrayList的大小是动态变化的。
  • ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。
  • 对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

 Arraylist、LinkedList 和 Vector 的区别

  ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。
  LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率低。
  Vector 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。
  Stack 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。

LinkedList
LinkedList是一个实现了List接口和Deque接口的双向链表。 LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法。LinkedList类中的一个内部私有类private static class Node<E>。

Arraylist 与 LinkedList 区别
Arraylist底层使用的是数组,支持快速随机访问,但是插入删除特定位置效率低,LinkedList底层使用的是双向循环链表数据结构插入,删除效率特别高,时间复杂度近似 O(1)。因此当数据特别多,而且经常需要插入删除元素时建议选用LinkedList。Arraylist是使用最多的集合类。 内存空间占用: ArrayList的空间浪费主要是在list列表的结尾会预留一定的容量空间,而LinkedList的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

ArrayList和LinkedList都实现了List接口,他们有以下的不同点:

ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。

相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。

LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

ArrayList 与 Vector 区别
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector ,代码要在同步操作上耗费大量的时间。Arraylist不是同步的,所以在不需要同步时建议使用Arraylist。

HashMap 简介

HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。

底层数据结构分析

JDK1.8之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话直接覆盖,不相同就通过拉链法解决冲突。

拉链法:遇到哈希冲突,将冲突的值加到链表中即可

扰动函数指的就是HashMap的 hash方法。使用 hash方法是为了防止一些实现比较差的 hashCode()方法, 换句话说使用扰动函数之后可以减少碰撞。

JDK1.7 HashMap 的 hash方法源码,相比于 JDK1.8的 hash 方法 ,JDK 1.7 的性能会稍差一点点,因为毕竟扰动了 4 次。

static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK 1.8 的 hash方法相比于 JDK 1.7 hash方法更加简化,但是原理不变。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

JDK1.8之后

相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

JDK1.8之后的HashMap底层数据结构 红黑树

红黑树特点

  • 每个节点非红即黑;
  • 根节点总是黑色的;
  • 每个叶子节点都是黑色的空节点(NIL节点);
  • 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
  • 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

红黑树的应用
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。

为什么要用红黑树
简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

红黑树这么优秀,为何不直接使用红黑树得了?
红黑树属于(自)平衡二叉树,但是为了保持“平衡”是需要付出代价的,红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡。而引入红黑树就是为了查找数据快,如果链表长度很短的话,根本不需要引入红黑树的。通过概率统计,长度8这个值是综合查询成本和新增元素成本得出的最好的一个值。

HashMap相关问题

问:HashMap 中的 key 如果是 Object 则需要实现哪些方法?
答:hashCode 方法和 equals 方法。hashCode 方法用来计算 Entry 在数组中的 index 索引位置,equals 方法用来比较数组指定 index 索引位置上链表的节点 Entry 元素是否相等。否则由于 hashCode 方法实现不恰当会导致严重的 hash 碰撞,从而使 HashMap 会退化成链表结构而影响性能。

问:为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键,即为什么使用它们可以减少哈希碰撞?
答:因为 String、Integer 等包装类是 final 类型的,具有不可变性,而且已经重写了 equals() 和 hashCode() 方法。不可变性保证了计算 hashCode() 后键值的唯一性和缓存特性,不会出现放入和获取时哈希码不同的情况且读取哈希值的高效性,此外官方实现的 equals() 和 hashCode() 都是严格遵守相关规范的,不会出现错误。

问:HashMap 的两种遍历
答:HashMap 的这两种遍历是分别对 keySet 和 entrySet 进行迭代,对于 keySet 实质上是遍历了两次,一次是转为 iterator 迭代器遍历,一次是从 HashMap中取出 key所对应的 value 操作;而 entrySet 方式只遍历了一次,它把 key和value都放到了 Entry 中,所以效率高。

问:HashMap 的工作原理是什么?
答:使用 put(key, value) 存储对象到 HashMap 中,使用 get(key) 从 HashMap 中获取对象。
1. 以下是具体的 put 过程(JDK1.8):
    a. 对Key求Hash值,然后再计算下标;
    b. 如果没有碰撞(即Hash值不同),直接放入桶中
    c. 如果碰撞了,以链表的方式链接到后面。如果链表长度超过阀值(TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表。
    d. 如果节点已经存在就替换旧值。
    e. 如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)。
2. 以下是具体 get 过程:
    调用 get() 方法,HashMap 会使用键对象的 hashcode 找到 bucket 位置,找到 bucket 位置之后,会调用 keys.equals() 方法去找到链表中正确的节点,最终找到要找的值对象。

解决冲突主要有三种方法:定址法,拉链法,再散列法。HashMap是采用拉链法解决哈希冲突的。
开放定址法:当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止。
拉链法:遇到哈希冲突,将冲突的值加到链表中即可。
若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。拉链法适合未规定元素的大小。

如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

HashMap 的长度为什么是2的幂次方
使用hash值计算数组下标的方法是(n - 1) & hash。当数组长度为2的n次幂的时候,不同的key计算得到的index相同的几率较小,减少Hash碰撞,那么数据在数组上分布就比较均匀。HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):

int capacity = 1;  
while (capacity < initialCapacity)   
    capacity <<= 1;  

Hashtable、HashMap 和 ConcurrentHashMap的区别

HashTable

  • Hashtable继承自Dictionary类,实现了Map接口。
  • 底层数组+链表实现,key和value都不能为null,往HashTable中放入null键或null值,会抛出NullPointerException。
  • 线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
  • HashTable直接使用对象的hashCode作为hash值
  • 初始size为11,扩容时将容量变为原来的2倍加1

HashMap

  • HashMap继承自AbstractMap类,实现了Map接口。
  • 底层数组+链表实现,可以存储null键和null值,但只能有一个null键。当get()方法返回null值时,可能是HashMap中没有该键,也可能是该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
  • 线程不安全,效率要高于HashTable。
  • HashMap重新计算hash值
  • 初始size为16,扩容时将容量变为原来的2倍,size一定为2的n次幂。扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入

ConcurrentHashMap

  • 底层采用分段的数组+链表实现,key和value都不能为null
  • 把整个Map分为N个Segment(N默认是16),同样提供线程安全,但是效率提升N倍。读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。
  • 线程安全,允许多个修改操作并发进行,其关键在于使用了锁分离技术。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
  • 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,避免无效扩容
  • ConcurrentHashMap性能是明显优于Hashtable和SynchronizedMap的

SynchronizedMap

  • 线程安全,一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为map。

hashCode()介绍
hashCode() 的作用是获取哈希码,也称为散列码,它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object中

hashCode()与equals()的联系
若两个对象相等,则hashcode一定是相同的,调用equals方法都返回true
若两个对象有相同的hashcode值,它们也不一定是相等的
若equals 方法被覆盖,则 hashCode 方法也必须被覆盖

==和eqauls()的区别
==:可用于基本类型和引用类型。当用于基本类型时候,是比较值是否相同;当用于引用类型的时候,是比较两个对象的地址是否相同。
eqauls:基本类型没有equals方法,equals用来比较的是两个对象的内容是否相等。一个类如果没有定义equals方法,它将默认继承Object中的equals方法,返回值与==方法相同。

comparable 和 comparator的区别
comparable接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序

什么是迭代器(Iterator)
Iterator接口提供了很多对集合元素进行迭代的方法。迭代器可以在迭代的过程中删除底层集合的元素。
 
Iterator和ListIterator的区别
Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引等等。

关键字

final,finalize和finally的不同之处
1. final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。
    被final修饰的类不可以被继承,final类中的所有成员方法都会被隐式地指定为final方法。
    被final修饰的方法不可以被重写,类中所有的private方法都隐式地指定为final。
    被final修饰的变量不可以被改变。如果是基本数据类型的变量,意味着该变量的值在初始化后不能被改变;如果修饰引用,那么表示引用不可变,引用指向的内容可变.
2. finalize一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并在下一次垃圾回收时才真正回收对象占用的内存。一般在该方法中释放对象持有的资源。
3. finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。

static 关键字

  • 修饰成员变量和成员方法: 被static修饰的成员属于类,不属于这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static声明的成员变量属于静态成员变量,静态变量存放在Java 内存区域的方法区。
  • 静态代码块:静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块->非静态代码块->构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
  • 静态内部类:static修饰类的话只能修饰内部类, 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。
  • 静态导包:import static用来导入类中的静态资源,1.5之后的新特性 。

初始化顺序

  1. 父类--静态变量
  2. 父类--静态初始化块
  3. 子类--静态变量
  4. 子类--静态初始化块
  5. 父类--变量
  6. 父类--初始化块
  7. 父类--构造器
  8. 子类--变量
  9. 子类--初始化块
  10. 子类--构造器

this、super关键字
this关键字用于引用类的当前实例。
super关键字用于从子类访问父类被子类隐藏的变量或覆盖的方法。
使用this和super要注意的问题:
  super()和this()均需放在构造方法内第一行
  this、super均不可以在static环境中使用,包括static变量、static方法、static语句块。 this和super是属于对象范畴的东西,而staitc是属于类范畴的东西。

成员变量与局部变量的区别

  1. 从语法上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是成员变量和局部变量都能被 final 所修饰;
  2. 从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量存在于栈内存
  3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显示地赋值);而局部变量则不会自动赋值。

& 和 &&的区别
首先记住&是位操作,而&&是逻辑运算符.另外逻辑运算符具有短路特性,而&不具备短路特性.

反射机制介绍

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
静态编译:在编译时确定类型,绑定对象;动态编译:运行时确定类型,绑定对象

反射机制优缺点
优点: 运行期类型的判断,动态加载类,提高代码灵活度。
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。

反射的应用场景
反射是框架设计的灵魂。在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;
②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:
 1) 将程序内所有 XML 或 Properties 配置文件加载入内存中;
 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
 3)使用反射机制,根据这个字符串获得某个类的Class实例;
 4)动态配置实例的属性

异常处理

Java中的异常处理可以提高系统的健壮性以及用户体验。Throwable是所有异常和错误的父类,其继承结构如下:

Java中的两种异常类型是什么?他们有什么区别?
a) Unchecked Exception
  非检查异常不要求程序员捕获,且可以由系统自动抛出, 如除法运算中除数为0时,程序自动抛出ArithmeticException的算术异常,而不用程序员手动抛出。RuntimeException表示代码本身存在BUG。
b) Checked Exception
  检查异常强制要求程序员捕获异常,否则编译不通过,假如将 SQLException 定义为非检测异常,开发人员可能不会捕获处理异常。这样操作数据发生异常时,会导致严重的 Connection 不关闭、Transaction 不回滚、DB 中出现脏数据等情况。将SQLException定义为检测异常,才会驱使开发人员去显式捕捉,并且在代码产生异常后清理资源。当然清理资源后,可以继续抛出非检测异常,阻止程序的执行。检测异常大多可以应用于工具类中。Java提供的检查异常机制,使得调用时可以明确知道该函数必须用try-catch捕获,而不用查看源代码才知该函数中存在异常。

Java中Exception和Error有什么区别?
Exception和Error都是Throwable的子类。Exception用于用户程序可以捕获的异常情况。Error定义了不期望被用户程序捕获的异常。

throw和throws有什么区别?
throw关键字用来在程序中明确的抛出异常,相反,throws语句用来表明方法不能处理的异常。每一个方法都必须要指定哪些异常不能处理,所以方法的调用者才能够确保处理可能发生的异常,多个异常是用逗号分隔的。

异常处理的时候,finally代码块的重要性是什么?
无论是否抛出异常,finally代码块总是会被执行。就算是没有catch语句同时又抛出异常的情况下,finally代码块仍然会被执行。最后要说的是,finally代码块主要用来释放资源,比如:I/O缓冲区,数据库连接。

异常处理完成以后,Exception对象会发生什么变化?
Exception对象会在下一个垃圾回收过程中被回收掉。

finally代码块和finalize()方法有什么区别?
无论是否抛出异常,finally代码块都会执行,它主要是用来释放应用占用的资源。finalize方法是Object类的一个protected方法,它是在对象被垃圾回收之前由Java虚拟机来调用的。

java序列化和反序列化

对象序列化的目标是将对象保存在磁盘中或者在网络中进行传输。实现的机制是允许将对象转为与平台无关的二进制流。java中对象的序列化机制是将允许对象转为字节序列。这些字节序列可以使Java对象脱离程序存在,从而可以保存在磁盘上,也可以在网络间传输。对象的序列化是将一个Java对象写入IO流;与此对应的,反序列化则是从IO流中恢复一个Java对象。要将一个java对象序列化,那么对象的类需要实现Serializable或者Externalizable接口。

使用Serializable序列化
如果一个类能被序列化,那么它的子类也能够被序列化。
保证序列化对象的引用对象的类也是可序列化的,如不可序列化,可以使用transient关键字进行修饰,否则会序列化失败;
static和transient声明的成员变量是不能被序列化的。在变量声明前加上transient关键字,可以阻止该变量被序列化到文件中。transient只能修饰属性,不能修饰类或方法。

使用Externalizable序列化
外部序列化与序列化的主要区别在于序列化是内置的API,只需实现Serializable接口,不需要编写任何代码就可以实现对象的序列化,而使用外部序列化时,Externalizable接口中的方法必须由开发人员实现。因此与实现Serializable接口的方法相比,使用Externalizable编写程序的难度更大但编程时可灵活控制需要持久化的属性。

版本问题serialVersionUID
每个可序列化的类都有一个都有一个唯一的标识号与其关联,也就是我们通常说的 serialVersionUID。serialVersionUID是为了防止序列化出错,序列化时要保持版本号的一致。

可序列化的类没有显示的声明该常量serialVersionUID,会带来什么问题
1. Java 虚拟机会自动根据这个类调用一个复杂的运算过程,从而产生运行时的 serialVersionUID。这个自动生成的值将会受到类名、实现的接口名称、以及所有的公有方法和受保护的成员名称所影响。如果你通过任何方式改变了这些信息,兼容性都会遭到破坏,在运行时会导致 InvalidClassException 异常。
2. 执行序列化和反序列化时有可能会遇到JRE版本问题。尤其是在网络的两端进行通信时,这种情况更为多见。如果你没有显示的声明 serialVersionUID,就算类的信息没有发生改变,不同 Java 虚拟机序列化的实现方式上可能有所不同,这些细微的差别都有可能导致运行时生成的 serialVersionUID 不一致从而导致反序列化失败。
为了解决这种问题,Java允许为序列化的类提供一个serialVersionUID的常量标识该类的版本。只要serialVersionUID的值不变,Java就会把它们当作相同的序列化版本。

自定义serialVersionUID的3个优点:
1. 提高程序运行效率。如果在类中未显示声明serialVersionUID,那么在序列化时会通过计算得到一个serialVersionUID。通过显示声明serialVersionUID的方式省去了计算的过程,提高了程序效率。
2. 提高程序不同平台上的兼容性。由于各个平台计算serialVersionUID的方式可能不同,通过显示的方式可以完全避免该问题。
3. 增强程序各个版本的可兼容性。在默认情况下每个类都有唯一的一个serialVersionUID,因此当后期对类进行修改时,类的serialVersionUID值将会发生变化,这将会导致类在修改前对象的文件在修改后无法进行反序列化操作。同样通过显示声明serialVersionUID也会解决该问题。

BIO,NIO,AIO 总结

同步与异步,区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
(1) 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
(2) 异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
阻塞和非阻塞
(1) 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
(2) 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

举个生活中简单的例子,烧水时等着水开(同步阻塞);边做别的事边时不时来看看水开了没有(同步非阻塞);如果水开了会发出声音,你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情(异步非阻塞)。

1. BIO (Blocking I/O)
  同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如下图所示。

传统BIO通信模型图

如果要让BIO通信模型能够同时处理多个客户端请求,就必须使用多线程(主要是 涉及的三个主要函数socket.accept()socket.read()socket.write()都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 线程池机制 改善,线程池还可以让线程的创建和回收成本相对较低。使用FixedThreadPool可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M)

当客户端并发访问量增加后这种模型会出现什么问题?
在 Java 虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。

2. 伪异步 IO

采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层任然是同步阻塞的BIO模型,因此无法从根本上解决问题。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

伪异步IO模型图

2.NIO

NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

NIO核心组件
(1) Buffer(缓冲区)
IO 面向流,而 NIO 面向缓冲区。Buffer是一个对象,任何时候访问NIO中的数据,都是通过缓冲区进行操作。
(2) Channel (通道)
NIO 通过Channel(通道) 进行读写。通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
(3) Selectors(选择器)
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。

为什么大家都不愿意用JDK原生NIO进行开发呢?
从上面的代码中大家都可以看出来,是真的难用!除了编程复杂、编程模型难之外,它还有以下让人诟病的问题。Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。
(1) JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%
(2) 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug

NIO和IO的主要区别

(1) 面向流与面向缓冲
    Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
(2) 阻塞与非阻塞IO
    Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
(3) 选择器(Selectors)
    Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

4. AIO (Asynchronous I/O)
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。

posted @   安小  阅读(195)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示