600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > java常见面试题答案

java常见面试题答案

时间:2018-09-06 21:41:13

相关推荐

java常见面试题答案

文章目录

java面试题&答案1、介绍自己做过的项目2、HashMap(重点)2.1 HashMap简介2.2 底层数据结构分析2.2.1 HashMap源码分析 2.3 HashMap和Hashtable的区别2.3.1 父类不同2.3.2 对外提供的接口不同2.3.3 对null的支持不同2.3.4 安全性不同2.3.5 初始容量大小和扩容机制不同2.3.6 计算hash值的方法不同 3、String相关3.1 String不可变性3.2 StringBuilder和StringBuffer的区别和联系3.3 String与StringBuilder3.4 final的使用场景 4、java中的设计模式4.1 解释熟悉的设计模式4.1.1 动态代理 5、数据库相关5.1 SQL语句是否熟练,复杂度较高的sql掌握如何?5.2 写出数据库索引、视图、存储过程、触发器5.3 简述SQL中的join有几种 6、JVM6.1 Class.forName和ClassLoader的区别(重点)6.1.1 类加载的过程6.1.2 类加载的7步 6.2 简述GC6.3 简述java中的GC机制两种底层算法原理6.3.1 标记-清除6.3.2 复制算法6.3.3 标记-整理6.3.4 分代收集 6.4 简述JVM6.5 双亲委派模型6.6 判断对象是否可回收的两种方式 7、线程7.1 线程的生命周期7.2 线程的实现方式7.2.1 继承Thread类7.2.2 实现Runnable接口7.2.3 实现Callable接口并搭配FutureTask7.2.4 线程池实现7.2.5 四种实现方式的特点 7.3 线程池有几种?7.3.1 4种线程池7.3.2 阿里巴巴手册并发处理(了解)7.3.3 线程池的概念7.3.4 线程池的优势和缺点 7.4 死锁7.4.1 写一个死锁 7.5 sleep和wait的区别?哪个会释放资源?为什么? 8、 抽象类和接口的区别?各有什么应用?8.1 抽象类的定义8.2 接口的定义8.3 相同点8.4 不同点8.5 注意事项8.6 应用 数据结构和算法1、线性表1.1.1 数组和链表的误区 javaEE1、Servlet相关1.1 forward和redirect1.1.1 forward1.1.2 redirect 1.2 include指令和include动作1.2.1 include指令(静态包含)1.2.2 include动作(动态包含)1.2.3 二者区别 1.3 Cookie和Session的区别 2、MVC2.1 Model2.2 View2.3 Controller 附录专有名词2、MVC2.1 Model2.2 View2.3 Controller 附录专有名词

java面试题&答案

作者:冯劲松(资源来自网络、jdk官方文档、github、csdn、简书、博客园、思否、《阿里巴巴java开发手册》、《java程序员面试笔试宝典》等)

时间:-11-6

注意:以下解析基于 jdk1.8

1、介绍自己做过的项目

2、HashMap(重点)

来源:github地址

2.1 HashMap简介

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

2.2 底层数据结构分析

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。 JDK 1.8 HashMap 的 hash 方法源码: JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。

static final int hash(Object key) {int h;// key.hashCode():返回散列值也就是hashcode// ^ :按位异或// >>>:无符号右移,忽略符号位,空位都以0补齐return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

对比一下 JDK1.7的 HashMap 的 hash 方法源码.

static int hash(int h) {// This function ensures that hashCodes that differ only by// constant multiples at each bit position have a bounded// number of collisions (approximately 8 at default load factor).h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);}

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

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

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

类的属性:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {// 序列号private static final long serialVersionUID = 362498820763181265L; // 默认的初始容量是16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 当桶(bucket)上的结点数大于这个值时会转成红黑树static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表static final int UNTREEIFY_THRESHOLD = 6;// 桶中结构转化为红黑树对应的table的最小大小static final int MIN_TREEIFY_CAPACITY = 64;// 存储元素的数组,总是2的幂次倍transient Node<k,v>[] table; // 存放具体元素的集transient Set<map.entry<k,v>> entrySet;// 存放元素的个数,注意这个不等于数组的长度。transient int size;// 每次扩容和更改map结构的计数器transient int modCount; // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容int threshold;// 填充因子final float loadFactor;}

loadFactor加载因子

loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,load Factor越小,也就是趋近于0,

loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

这一点在《阿里巴巴java开发手册》中也有体现,其建议使用集合时,给其一个初始化的容量

threshold

threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。

Node节点类源码:

// 继承自 Map.Entry<K,V>static class Node<K,V> implements Map.Entry<K,V> {final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较final K key;//键V value;//值// 指向下一个节点Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey() {return key; }public final V getValue(){return value; }public final String toString() {return key + "=" + value; }// 重写hashCode()方法public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}// 重写 equals() 方法public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}}

树节点类源码:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent; // 父TreeNode<K,V> left; // 左TreeNode<K,V> right; // 右TreeNode<K,V> prev; // needed to unlink next upon deletionboolean red; // 判断颜色TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}// 返回根节点final TreeNode<K,V> root() {for (TreeNode<K,V> r = this, p;;) {if ((p = r.parent) == null)return r;r = p;}

2.2.1 HashMap源码分析

构造方法

// 默认构造函数。public More ...HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}// 包含另一个“Map”的构造函数public More ...HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);//下面会分析到这个方法}// 指定“容量大小”的构造函数public More ...HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}// 指定“容量大小”和“加载因子”的构造函数public More ...HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " + loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}

putMapEntries方法:

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {// 判断table是否已经初始化if (table == null) {// pre-size// 未初始化,s为m的实际元素个数float ft = ((float)s / loadFactor) + 1.0F;int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);// 计算得到的t大于阈值,则初始化阈值if (t > threshold)threshold = tableSizeFor(t);}// 已初始化,并且m元素个数大于阈值,进行扩容处理else if (s > threshold)resize();// 将m中的所有元素添加至HashMap中for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();putVal(hash(key), key, value, false, evict);}}}

put方法HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。

对putVal方法添加元素的分析如下:

①如果定位到的数组位置没有元素 就直接插入。 ②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入。

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// table未初始化或者长度为0,进行扩容if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);// 桶中已经存在元素else {Node<K,V> e; K k;// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 将第一个元素赋值给e,用e来记录e = p;// hash值不相等,即key不相等;为红黑树结点else if (p instanceof TreeNode)// 放入树中e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 为链表结点else {// 在链表最末插入结点for (int binCount = 0; ; ++binCount) {// 到达链表的尾部if ((e = p.next) == null) {// 在尾部插入新结点p.next = newNode(hash, key, value, null);// 结点数量达到阈值,转化为红黑树if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);// 跳出循环break;}// 判断链表中结点的key值与插入的元素的key值是否相等if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))// 相等,跳出循环break;// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表p = e;}}// 表示在桶中找到key值、hash值与插入元素相等的结点if (e != null) {// 记录e的valueV oldValue = e.value;// onlyIfAbsent为false或者旧值为nullif (!onlyIfAbsent || oldValue == null)//用新值替换旧值e.value = value;// 访问后回调afterNodeAccess(e);// 返回旧值return oldValue;}}// 结构性修改++modCount;// 实际大小大于阈值则扩容if (++size > threshold)resize();// 插入后回调afterNodeInsertion(evict);return null;}

我们再来对比一下 JDK1.7 put方法的代码

对于put方法的分析如下:

①如果定位到的数组位置没有元素 就直接插入。 ②如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素。

public V put(K key, V value)if (table == EMPTY_TABLE) {inflateTable(threshold); } if (key == null)return putForNullKey(value);int hash = hash(key);int i = indexFor(hash, table.length);for (Entry<K,V> e = table[i]; e != null; e = e.next) {// 先遍历Object k;if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue; }}modCount++;addEntry(hash, key, value, i); // 再插入return null;}

get方法

public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 数组元素相等if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;// 桶中不止一个节点if ((e = first.next) != null) {// 在树中getif (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 在链表中getdo {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;}

resize方法进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {// 超过最大值就不再扩充了,就只好随你碰撞去吧if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 没超过最大值,就扩充为原来的2倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 计算新的resize上限if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {// 把每个bucket都移动到新的buckets中for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else {Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// 原索引if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}// 原索引+oldCapelse {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 原索引放到bucket里if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 原索引+oldCap放到bucket里if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}

2.3 HashMap和Hashtable的区别

来源:csdn链接 简书链接

2.3.1 父类不同

Hashtable定义:

public class Hashtable<K,V>extends Dictionary<K,V>implements Map<K,V>, Cloneable, java.io.Serializable

HashMap定义:

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable

2.3.2 对外提供的接口不同

Hashtable比HashMap多提供了elments() 和contains() 两个方法。

elments() 方法继承自Hashtable的父类Dictionnary。elements() 方法用于返回此Hashtable中的value的枚举。

contains()方法判断该Hashtable是否包含传入的value。它的作用与containsValue()一致。事实上,contansValue() 就只是调用了一下contains() 方法。

2.3.3 对null的支持不同
2.3.4 安全性不同

虽然HashMap是线程不安全的,但是它的效率远远高于Hashtable,这样设计是合理的,因为大部分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap

ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。

2.3.5 初始容量大小和扩容机制不同

分析:

创建时,如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。也就是说Hashtable会尽量使用素数、奇数。而HashMap则总是使用2的幂作为哈希表的大小。

之所以会有这样的不同,是因为HashtableHashMap设计时的侧重点不同。Hashtable的侧重点是哈希的结果更加均匀,使得哈希冲突减少。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。而HashMap则更加关注hash的计算效率问题。在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。HashMap为了加快hash的速度,将哈希表的大小固定为了2的幂。当然这引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改动。这从而导致了HashtableHashMap的计算hash值的方法不同。

2.3.6 计算hash值的方法不同

源代码中的体现:

HashMap中的hash方法

static final int hash(Object key) {int h;// key.hashCode():返回散列值也就是hashcode// ^ :按位异或// >>>:无符号右移,忽略符号位,空位都以0补齐return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

Hashtable中并未定义hash方法,直接调用对象的hashCode方法,下边使用put方法为例:

这里可以看到存储时,hash的计算直接调用了Object类的hashCode方法。而且其key不能为null,因为如果为null就会抛出NPE。

public synchronized V put(K key, V value) {// Make sure the value is not nullif (value == null) {throw new NullPointerException();}// Makes sure the key is not already in the hashtable.Entry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;@SuppressWarnings("unchecked")Entry<K,V> entry = (Entry<K,V>)tab[index];for(; entry != null ; entry = entry.next) {if ((entry.hash == hash) && entry.key.equals(key)) {V old = entry.value;entry.value = value;return old;}}addEntry(hash, key, value, index);return null;}

3、String相关

3.1 String不可变性

参考:思否 、 csdn

很多人的错误理解,String不可变是因为final。确实是因为final关键字,但不是定义类时的那个final。

String类的定义:此处的final只是为了该类不可被继承。

public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence

真正起作用的是String类中的数组是用final定义的:

/** The value is used for character storage. */private final char value[];

所以当数组一旦被初始化,就不能再被重新赋值了。而且String的大多数方法在对其进行操作时,都是返回了一个新的String对象。String类也不对外部提供方法去操作这个字符数组。这一系列的因素导致了String对象一经构造就不再变的特性。另外,破坏这个特性可以使用java中的反射机制。

3.2 StringBuilder和StringBuffer的区别和联系

参考:百度

StringBuffer是为了解决大量字符串拼接时产生的很多中间对象问题而提供的一个类。主要提供append和add方法。本质是一个线程安全的可修改的字符序列,把所有的修改数据的方法都加上了synchronized。但是保证了线程安全是需要性能的代价的。

因此,设计了StringBuilder(jdk1.5出现),它和StringBuffer之间本质上没什么差别,只是去掉了线程安全的部分,减少了开销。

从源代码的角度看:

public final class StringBufferextends AbstractStringBuilderimplements java.io.Serializable, CharSequence

public final class StringBuilderextends AbstractStringBuilderimplements java.io.Serializable, CharSequence

类的定义,可以看出它们的父类相同。修改数据的方法实现都是调用父类的方法,只是StringBuilder没加锁,StringBuffer加了锁。

方法的差异,举一个例子

StringBuffer中:

@Overridepublic synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;}

StringBuilder中:

@Overridepublic StringBuilder append(String str) {super.append(str);return this;}

3.3 String与StringBuilder

这个涉及到效率问题,在《阿里巴巴java开发手册》中也有体现。

举个例子:

package org.feng.string;public class StringDemo {public static void main(String[] args) {String temp = "123";temp += "456";temp += "789";System.out.println(temp + "!");}}

在命令行中使用javap -c StringDemo.class,做这一步,先去使用javac编译java文件。

D:\jee--12-workspace\mianshi_demo\src\org\feng\string>javap -c StringDemo.classCompiled from "StringDemo.java"public class org.feng.string.StringDemo {public org.feng.string.StringDemo();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: ldc #2 // String 1232: astore_13: new #3 // class java/lang/StringBuilder6: dup7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V10: aload_111: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;14: ldc #6 // String 45616: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;22: astore_123: new #3 // class java/lang/StringBuilder26: dup27: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V30: aload_131: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;34: ldc #8 // String 78936: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;39: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;42: astore_143: getstatic#9 // Field java/lang/System.out:Ljava/io/PrintStream;46: new #3 // class java/lang/StringBuilder49: dup50: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V53: aload_154: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;57: ldc #10 // String !59: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;62: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;65: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V68: return}

发现:每次字符串拼接,都会new一个StringBuilder对象。因此,在大量的字符串拼接时使用StringBuilder和它的append方法来代替+拼接字符串。

3.4 final的使用场景

参考:《阿里巴巴java开发手册》

final可以声明类、成员变量、方法及本地变量,下列情况可以使用final关键字:

不允许被继承的类,如:String类不允许修改引用的域对象,如:POJO类的域变量不允许被重写的方法,如:POJO类的setter方法不允许运行过程中重新赋值的局部变量避免上下文重复使用一个变量,使用final描述可以强制重新定义一个变量,方便更好地进行重构

4、java中的设计模式

迭代器设计模式(Iterator接口):JDK类库中,Collection接口的iterator方法会返回一个Iterator类型的对象,而其子接口List的listIterator方法返回一个listIterator类型的对象,ListIteratorIterator的子类。他们构成了java语言对迭代器模式的支持。

装饰器设计模式:java的IO部分使用到了。InputStreamOutputStreamReaderWriter等都使用了该模式。举例:FileInputStream是对InputStream的一个装饰,没有InputStream就不能使用FileInputStream。装饰模式用于动态给对象增加一些额外的职责,是一种结构型设计模式。

适配器设计模式(Adapter):在Spring AOP中,对BeforeAdviceAfterAdviceThrowsAdvice三种通知类型借助适配器模式来实现。这样的好处是使得框架允许用户向框架中加入自己想要支持的任何一种通知类型。提供了AdvisorAdapter接口。另外,java的JDBC也是一种适配器设计模式的应用。抽象的JDBC接口和各个数据库引擎API之间都需要相应的适配器软件,这就是为各个不同数据库引擎准备的驱动程序。

工厂设计模式(Factory):java的集合中,迭代器的产生(iterator方法)就是一种工厂。JDBC中也使用了,创建Connection对象、StatementResultSet时,都使用了工厂方法。

单例设计模式(Singleton):java中的Runtime类是一个单例,Calendar类是一个单例;在Spring框架中bean可以设置为单例。

<bean id="date" class="java.util.Date" scope="singleton"/>

代理设计模式(Proxy):java中提供了Proxy类来实现代理。

观察者设计模式(Observer):java中提供了Observer接口和Subject接口来帮助实现观察者设计模式。观察者设计模式(也被称为发布/订阅模式),提供了避免组件之间紧密耦合的另一种方法,它将观察者和被观察者的对象分离开来。在该模式中,一个对象通过添加一个方法(该方法允许另一个对象,即观察者注册自己)使本身变得可观察。当可观察的对象更改时,它会将消息发送到已注册的观察者。

4.1 解释熟悉的设计模式

4.1.1 动态代理

参考博客

动态代理是一种较为高级的代理模式,它的典型应用是Spring AOP。

代理模式的优点在于能够协调调用者和被调用者,在一定程度上降低了系统的耦合;其缺点在于由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,并且实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

package org.feng.proxy;// 抽象主题public interface Subject {String getSubjectName();void say();}

package org.feng.proxy;// 真实主题public class RealSubject implements Subject {@Overridepublic String getSubjectName() {return this.getClass().getName();}@Overridepublic void say() {System.out.println("I am RealSubject.");}}

package org.feng.proxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;// 动态代理类:使用了Lambda表达式public class DynamicProxy {@SuppressWarnings("unchecked")public static final <T extends Subject> T getSubject(T subject) {return (T) Proxy.newProxyInstance(subject.getClass().getClassLoader(),subject.getClass().getInterfaces(),// Lambda代替实现了InvocationHandler接口(obj, method, args) -> {return method.invoke(subject, args);});}}/// 该Handler没被使用// 该类被Lambda表达式代替了class Handler<T> implements InvocationHandler {private T subject;public Handler(T subject) {this.subject = subject;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {return method.invoke(subject, args);}}

package org.feng.proxy;import java.util.Optional;// 客户端测试public class Client {public static void main(String[] args) {Subject subject = new RealSubject();Subject proxy = DynamicProxy.getSubject(subject);Optional.ofNullable(proxy.getSubjectName()).ifPresent(System.out::println);proxy.say();}}

5、数据库相关

5.1 SQL语句是否熟练,复杂度较高的sql掌握如何?

5.2 写出数据库索引、视图、存储过程、触发器

索引:给某列添加索引后,可以快速检索数据,减少磁盘检索次数。索引是自动维护,创建之后,自己维护索引项,查询时自动使用索引。

视图:已经编译且执行过的查询结果。就是一张假表。操作视图就是操作基表,操作基表就是操作视图。查询视图比查询基表更快捷。

存储过程:一次性执行多条sql语句。可以有复杂的逻辑,比如判断、循环、异常等

触发器:当执行一条增删改的sql语句时,定义自动执行另外的sql语句。可以分为前置触发、后置触发、语句触发。

5.3 简述SQL中的join有几种

left join:即使右表中没有匹配,也从左表返回所有的行

right join:即使左表中没有匹配,也从右表返回所有的行

full join:只要其中一个表中存在匹配,就返回行

6、JVM

java虚拟机(Java Virtual Machie ,JVM)

平台独立性:指可以在一个平台上编写和编译程序,而在其他平台上运行。保证java这一特性的正是java的字节码和java虚拟机。

java字节码的执行分两种方式:编译方式和解释执行方式。通常采用解释执行方式。

6.1 Class.forName和ClassLoader的区别(重点)

参考:《深入理解java虚拟机 JVM高级特性与最佳实践》

Class.forname除了将类的.class文件加载的JVM中之外,还会对类进行解释,执行类中的static块。

Class.loader只是将.class文件加载的JVM中,不会执行static块的内容,只有在newInstance才会去执行static块。

6.1.1 类加载的过程

特殊的:

说明:由以上5种场景,Class.forName是对类的主动引用,是会初始化类的;ClassLoader则属于被动引用,只生成了字节码。

6.1.2 类加载的7步

java 类加载需要经历一下 7 个过程:

加载

类加载的第一个过程,在这个阶段,将完成以下三件事情:

通过一个类的全限定名获取该类的二进制流。将该二进制流中的静态存储结构转化为方法去运行时数据结构。在内存中生成该类的 Class 对象,作为该类的数据访问入口。

验证

验证的目的是为了确保 Class 文件的字节流中的信息不会危害到虚拟机,在该阶段主要完成以下四种验证:

文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟

机范围内,常量池中的常量是否有不被支持的类型.元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不

被继承的类等。字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,

确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转

指令是否正确等。符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执

行。

准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。

解析

该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

初始化

初始化是类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。

6.2 简述GC

在 java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收 。

6.3 简述java中的GC机制两种底层算法原理

备注:这道题可以自由发挥,题目中的两种,指的是标记清除和复制算法。但是真实环境下,是混合使用(即分代回收)的。

6.3.1 标记-清除

这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。

这种方法很简单,但是会有两个主要问题:

1.效率不高,标记和清除的效率都很低;

2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次 GC 动作。

6.3.2 复制算法

为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。于是将该算法进行了改进,内存区域不再是按照 1: 1 去划分,而是将内存划分为8:1:1 三部分,较大那份内存交 Eden 区,其余是两块较小的内存区叫 Survior 区。每次都会优先使用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,然后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对象通过分配担保机制复制到老年代中。 (java 堆又分为新生代和老年代)

6.3.3 标记-整理

该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。

6.3.4 分代收集

现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。

6.4 简述JVM

JVM包含运行时数据区(内存模型)、类装载子系统、执行引擎等。

运行时数据区包含:堆、栈(线程)、本地方法栈、方法区(元空间,包含常量,静态变量,类元信息)、程序计数器。

方法区:

有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生 GC,在这里进行的 GC 主要是对方法区里的常量池和对类型的卸载方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。该区域是被线程共享的。方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。

虚拟机栈:

虚拟机栈也就是我们平常所称的栈内存,它为 java 方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。虚拟机栈是线程私有的,它的生命周期与线程相同。局部变量表里存储的是基本数据类型、 returnAddress 类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式。每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。

本地方法栈

本地方法栈和虚拟机栈类似,只不过本地方法栈为 Native 方法服务。

java 堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。

程序计数器

内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个 java 虚拟机规范没有规定任何 OOM 情况的区域。

6.5 双亲委派模型

当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

6.6 判断对象是否可回收的两种方式

引用计数法

所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象” ,将会被垃圾回收。

引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法

引用链法(可达性分析)

从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。

java 中的GC ROOT

虚拟机栈中引用的对象方法区类静态属性引用的对象方法区常量池引用的对象本地方法栈 JNI 引用的对象

备注(了解):

虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记。如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。

如果该对象有必要执行 finalize()方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize()执行缓慢或者发生了死锁,那么就会造成 FQueue 队列一直等待,造成了内存回收系统的崩溃。 GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。

7、线程

线程是指程序在执行过程中,能够执行程序代码的执行单元。在java语言中,线程有4种状态:运行、就绪、挂起和结束。

进程是指一段正在执行的程序。而线程有时也被称为轻量级进程,它是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源(如打开的文件),但是各个线程拥有自己的栈空间。

运行于同一进程内的线程共享代码段、数据段,线程的启动或切换的开销要比进程少很多。

7.1 线程的生命周期

7.2 线程的实现方式

一般有4种:

继承Thread类,重写run方法实现Runnable接口(实现run方法)实现Callable接口,搭配FutureTask使用线程池

7.2.1 继承Thread类

package org.feng.thread;public class ThreadDemo extends Thread {@Overridepublic void run() {System.out.println("我重写了Thread的run方法!");}// 测试public static void main(String[] args) {new ThreadDemo().start();}}

7.2.2 实现Runnable接口

package org.feng.thread;public class RunnableDemo {// 测试public static void main(String[] args) {// 使用Lambda表达式代替Runnable接口的实现new Thread(()->{System.out.println("我是Runnable的run方法!");}).start();}}

7.2.3 实现Callable接口并搭配FutureTask

package org.feng.thread;import java.util.Arrays;import java.util.List;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;public class CallableDemo {// 测试public static void main(String[] args) throws InterruptedException, ExecutionException {// 使用Lambda表达式代替Callable的实现类FutureTask<List<String>> future = new FutureTask<List<String>>(() -> {List<String> strList = Arrays.asList("1", "2", "3", "4");return strList;});Thread thread = new Thread(future);// 启动线程thread.start();// 遍历结果future.get().forEach(System.out::println);}}

7.2.4 线程池实现

参见:7.3 线程池有几种?

7.2.5 四种实现方式的特点

7.3 线程池有几种?

参考:csdn博客、简书、线程池概念、《阿里巴巴java开发手册》

7.3.1 4种线程池

相关类:

java.util.concurrent.Executorsjava.util.concurrent.ThreadPoolExecutor

newFixedThreadPool 定长线程池

一个有指定的线程数的线程池,有核心的线程,里面有固定的线程数量,响应的速度快。正规的并发线程,多用于服务器。固定的线程数由系统资源设置。核心线程是没有超时机制的,队列大小没有限制,除非线程池关闭了核心线程才会被回收。

newCachedThreadPool 可缓冲线程池

只有非核心线程,最大线程数很大,每新来一个任务,当没有空余线程的时候就会重新创建一个线程,这边有一个超时机制,当空闲的线程超过60s内没有用到的话,就会被回收,它可以一定程序减少频繁创建/销毁线程,减少系统开销,适用于执行时间短并且数量多的任务场景。

ScheduledThreadPool 周期线程池

创建一个定长线程池,支持定时及周期性任务执行,通过过schedule方法可以设置任务的周期执行

newSingleThreadExecutor 单任务线程池

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,每次任务到来后都会进入阻塞队列,然后按指定顺序执行。

7.3.2 阿里巴巴手册并发处理(了解)

【强制】 获取单例对象需要保证线程安全,其中的方法也要保证线程安全。

说明: 资源驱动类、工具类、单例工厂类都需要注意。

【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 (说明: 使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者

“过度切换”的问题。 )

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回的线程池对象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool:

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2) CachedThreadPool 和 ScheduledThreadPool:

允许的创建线程数量为 Integer.MAX_VALUE, 可能会创建大量的线程,从而导致 OOM。

7.3.3 线程池的概念

本质上是一种对象池,用于管理线程资源。 在任务执行前,需要从线程池中拿出线程来执行。 在任务执行完成之后,需要把线程放回线程池。 通过线程的这种反复利用机制,可以有效地避免直接创建线程所带来的坏处。

7.3.4 线程池的优势和缺点

优点:

降低资源的消耗。线程本身是一种资源,创建和销毁线程会有CPU开销;创建的线程也会占用一定的内存。提高任务执行的响应速度。任务执行时,可以不必等到线程创建完之后再执行。提高线程的可管理性。线程不能无限制地创建,需要进行统一的分配、调优和监控。

缺点:

频繁的线程创建和销毁会占用更多的CPU和内存频繁的线程创建和销毁会对GC产生比较大的压力线程太多,线程切换带来的开销将不可忽视线程太少,多核CPU得不到充分利用,是一种浪费

7.4 死锁

多个线程因竞争资源而造成的一种僵局,若无外力作用,这些进程都无法向前推进。

产生原因:

竞争资源(非剥夺性)线程推进顺序非法

四个必要条件

互斥条件请求和保持条件不可剥夺条件环路等待条件

处理方法:

预防:通过破坏四个必要条件之一避免:不破坏死锁的产生的四个必要条件,在资源的动态分配中,防止线程进入可能发生死锁的不安全状态(银行家算法)检测解除死锁:允许系统出现死锁,但系统设置了检测机制及时检测出死锁;检测出死锁后,系统将采取措施解除死锁

7.4.1 写一个死锁

package org.feng.thread;public class DeadLockClient {// 启动线程public static void main(String[] args) {Thread thread1 = new Thread(new MyLock(true));Thread thread2 = new Thread(new MyLock(false));thread2.start();thread1.start();}}// 实现Runnableclass MyLock implements Runnable{boolean flag = false;public MyLock(boolean flag) {this.flag = flag;}@Overridepublic void run() {if(flag) {synchronized (MyDeadLock.lockA) {System.out.println("Lock A !");synchronized (MyDeadLock.lockB) {System.out.println("Lock B !");}}} else {synchronized (MyDeadLock.lockB) {System.out.println("Lock B !");synchronized (MyDeadLock.lockA) {System.out.println("Lock A !");}}}}}// 对象:充当锁class MyDeadLock{static Object lockA = new Object();static Object lockB = new Object();}

7.5 sleep和wait的区别?哪个会释放资源?为什么?

区别: 1. sleep是Thread的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动苏醒;

wait是Object的方法,用于线程间通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用notify()方法(或notifyAll()方法)时才醒来,也可以传入一个时间让它“自动”醒来。2. 最主要的区别是sleep不会释放资源;wait会释放资源

原因:它们对锁的处理机制不同。sleep方法的主要作用是让线程暂停执行一段时间,时间一到自动恢复,不涉及线程间通信,因此它不会释放锁。而调用wait方法,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。3. 使用区域不同。wait具有特殊含义,必须放在同步控制方法或同步语句块中;而sleep可以放在任何地方。

8、 抽象类和接口的区别?各有什么应用?

参考:FBB博客

8.1 抽象类的定义

抽象方法: 当多个类中出现相同功能,但是功能主体不同,这时可以进行向上抽取,这时,只抽取功能定义,不去抽取功能主体,这时就不需要写功能的主体了。这时的方法就是一个抽象方法(笼统的、模糊的、看不懂的),就需要去标识一下,使用abstract关键字,叫做抽象方法。

抽象方法是没有方法体,只有修饰符、返回值、方法名和参数列表的方法!使用abstract关键字修饰。

定义了抽象方法的类必须定义为抽象类,使用abstract关键字标识。

8.2 接口的定义

可以认为是一个特殊的抽象类(初期的理解),当抽象类中的方法全是抽象方法时,就可以把它定义成另外一种表现形式,叫做接口。接口用interface定义。

接口不能创建对象,需要被子类实现(必须实现接口中的所有方法)之后,才能创建对象。

另外在jdk1.8版本中,对接口进行了扩展,也就是增加了可以定义默认方法和静态方法。这两种都可以实现(有方法体)

8.3 相同点

1、都可以定义或者不定义抽象方法。

2、都可以定义常量。

3、都不能用final修饰。

4、都不能用static修饰。

5、都不能用private修饰。

6、抽象类可以被多个类所继承,接口也可以被多个类所实现。

7、抽象类之间可以存在继承关系,接口之间也可以存在继承关系。

8、都可以提高程序的复用性。

9、都不能new自己的对象,即使抽象类中可以定义构造方法也不行。

8.4 不同点

1、定义方式不同:接口使用interface关键字定义;抽象类则是需要用abstract关键字定义。

2、非抽象方法定义的不同:接口中只能定义抽象方法;抽象类中既可以定义抽象方法,还可以定义非抽象方法!

3、定义常量时的格式不同:接口中格式是固定的,都必须是public static final修饰的;而抽象类中则不做限制。

4、在构造方法上:抽象类中可以定义构造方法;接口中则不能定义构造方法。

5、一个类继承了抽象类,就必须重写其父类中的所有抽象方法,否则就必须把这个类也定义成抽象类;一个类实现了接口,就必须重写这个接口中的所有抽象方法,否则就得把这个类定义成抽象类。

6、抽象类为了被其子类或者子类的子类重写其抽象方法,就必须被继承;接口则需要被实现(implements)。

7、这条应该和第2条相对应的看,想要用接口中的方法就必须去实现其抽象方法;而抽象类中有非抽象的方法,其功能可以直接拿来用!

8、抽象类只能是单继承,换句话说,就是抽象类的父类只能有1个(当然还可以多层继承);接口可以继承多个接口(或者说成有多个父接口,这样能好理解点!)。总结成一句话就是,一个类可以实现多个接口,但是只能继承一个类。

8.5 注意事项

当一个类实现了多个接口时,这些接口中如果有相同的方法,那么只需要重写一次。

8.6 应用

抽象类的应用:适配器设计模式、模板方法设计模式

接口的应用:制定规则、Lambda表达式(函数式编程)

类是对事物的抽象,抽象类是对类的抽象,接口是对抽象类的抽象

接口用于抽象事物的特性,抽象类用于代码复用

当类之间存在is a关系时,可以定义为继承关系;当类存在has a关系时,可以抽象为接口

数据结构和算法

1、线性表

1.1.1 数组和链表的误区

数组和链表的区别?

链表适合插入、删除,时间复杂度为O(1)。

数组支持随机访问,根据下标随机访问的时间复杂度为O(1);

**(了解)**数组的插入,最好情况,在最后的位置插入,不用移动数组中其他元素,这时时间复杂度O(1),最坏的情况,往最开始的位置插入数据,这时所有元素都需要移动,因此时间复杂度是O(n)。因为往数组中每一个位置插入元素是不确定的因此平均的时间复杂度是(1 + 2 + 3 + …n)/n = O(n)。

误区:数组的查询速度其实并不快,即使使用二分查找,其时间复杂度也还有O(logn)

javaEE

参考:《java程序员面试笔试宝典》

1、Servlet相关

1.1 forward和redirect

在设计web应用程序时,经常需要把一个系统进行结构化设计,即按照模块进行划分,让不同的Servlet来实现不同的功能。为了实现这种程序的模块化,需要保证Servlet之间可以相互跳转,而跳转的方式一般有两种:forward和redirect。

1.1.1 forward

forward是服务器内部的重定向,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,而客户端并不知道,因此在客户端浏览器的地址栏不会显示转向后的地址,还是原来的地址。由于在整个定向的过程中用的是同一个Request,因此forward会将Request的信息带到被定向的JSP或Servlet中使用。

1.1.2 redirect

redirect则是客户端的重定向,是完全的跳转,即客户端浏览器会获取到跳转后的地址,然后重新发送请求,因此浏览器中会显示跳转后的地址。同时,由于这种方式比forward多了一次网络请求,因此其效率要低于forward方式。需要注意的是,客户端的重定向可以通过设置特定的HTTP头或写JavaScript脚本来实现。

1.2 include指令和include动作

include的主要作用是用来在当前文件中引入另一个文件,以便在当前文件中使用,例如,当应用程序中的所有页面的某些部分(例如标题、页脚、导航栏等)都一模一样时,就可以考虑把相同的部分提取出来写入一个单独的文件中,然后通过include方式引入。

include有两种使用方法:include指令和include动作。

1.2.1 include指令(静态包含)

include指令是编译阶段的指令,即在编译时,编译器会把指令所指向目标文件的内容复制到指令所在的位置,替换指令,最终形成一个文件,在运行时只有一个文件。也就是说,include指令所包含文件的内容是在编译时插入到JSP文件中的,当文件内容有变化时就要重新编译,因此适合于包含静态页面的情况,例如可以包含一个Servlet。

include指令的使用方法为:

<%@ include file="test.jsp" %>

1.2.2 include动作(动态包含)

include动作是运行时的语法,在主页面被请求时,才将用到的页面包含进来,涉及两个页面,类似于方法的调用。因此更适用于包含动态页面的情况。

include动作的使用方法为:

<jsp:include page="test.jsp" flush="true"><jsp:param name="name" value="value"/></jsp:include>

1.2.3 二者区别

include指令与include动作之间的根本性差异在于二者之间被调用的时间。

除此之外,还有:

当使用include动作时,在页面中声明的变量不可用于另一文件,除非将变量放置在request、session、application作用域中;而在使用include指令时,当前页面和被包含页面可以共享变量。当使用include指令时,新生成的JSP页面要符合JSP语法要求,应该避免变量名的冲突;而在使用include动作时不存在变量名冲突问题。include指令会修改被包含文件,但不会立即生效,除非修改页面或删除主页面的类;而include动作修改了被包含的文件,会立即生效。

1.3 Cookie和Session的区别

cookie是在HTTP下,服务器或脚本可以维护客户工作站上信息的一种方式。它是由Web服务器保存在用户浏览器上的小文件,可以包含有关用户的信息(如身份证号码、密码等信息)。session是指用来在客户端与服务端之间保持状态的解决方案以及存储结构。

区别:

cookie机制采用的是在客户端保持状态的方案,即数据存放在客户的浏览器上;而session机制采用的是在服务端保持状态的方案,即数据放在服务器上。cookie安全性不够。由于cookie信息存在客户端,其他人可以很容易地得到存放在本地的cookie,并进行cookie;而session信息存放在服务器端,因此较为安全。cookie性能更高一些。由于session信息存放在服务器端,因此当访问量增多时,会降低服务器的性能。单个cookie保存的数据不能超过4KB,很多浏览器都限制一个站点最多保存20个cookie;而session不存在此问题。

鉴于以上区别,一般将用户登陆信息等重要信息存放至session中,而其它需要保留的信息可以放在cookie中。

2、MVC

MVC是Model(模型)、View(视图)和Controller(控制器)。

MVC是一种目前广泛流行的应用模型,其目的是实现Web系统的职能分工。

2.1 Model

模型表示企业数据和业务逻辑,它是应用程序的主体部分。(用于封装数据)

2.2 View

视图是用户看到的并与之交互的界面。(获取和展示信息)

视图主要用于:根据客户类型显示信息,显示商品逻辑(模型)结构,而不关心信息如何获得何时获得。

2.3 Controller

控制器接收用户的输入并调用模型和视图去完成用户的需求。(流程控制)

附录

参考:《阿里巴巴java开发手册》

专有名词

POJO( Plain Ordinary Java Object) : 在手册中, POJO 专指只有 setter / getter /toString 的简单类,包括 DO/DTO/BO/VO 等。GAV( GroupId、 ArtifactctId、 Version) : Maven 坐标,是用来唯一标识 jar 包。OOP( Object Oriented Programming) : 本手册泛指类、对象的编程处理方式。ORM( Object Relation Mapping) : 对象关系映射,对象领域模型与底层数据之间的转换,本文泛指 iBATIS, mybatis 等框架。NPE( java.lang.NullPointerException) : 空指针异常。SOA( Service-Oriented Architecture) : 面向服务架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用, 有利于提升组件可重用性,可维护性。IDE( Integrated Development Environment) : 用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具, 本《手册》 泛指 IntelliJ IDEA 和eclipse。OOM( Out Of Memory) : 源于 java.lang.OutOfMemoryError, 当 JVM 没有足够的内存来为对象分配空间并且垃圾回收器也无法回收空间时, 系统出现的严重状况。一方库: 本工程内部子项目模块依赖的库( jar 包) 。二方库: 公司内部发布到中央仓库,可供公司内部其它应用依赖的库( jar 包) 。三方库: 公司之外的开源库( jar 包) 。

态的方案,即数据放在服务器上。

cookie安全性不够。由于cookie信息存在客户端,其他人可以很容易地得到存放在本地的cookie,并进行cookie;而session信息存放在服务器端,因此较为安全。cookie性能更高一些。由于session信息存放在服务器端,因此当访问量增多时,会降低服务器的性能。单个cookie保存的数据不能超过4KB,很多浏览器都限制一个站点最多保存20个cookie;而session不存在此问题。

鉴于以上区别,一般将用户登陆信息等重要信息存放至session中,而其它需要保留的信息可以放在cookie中。

2、MVC

MVC是Model(模型)、View(视图)和Controller(控制器)。

MVC是一种目前广泛流行的应用模型,其目的是实现Web系统的职能分工。

2.1 Model

模型表示企业数据和业务逻辑,它是应用程序的主体部分。(用于封装数据)

2.2 View

视图是用户看到的并与之交互的界面。(获取和展示信息)

视图主要用于:根据客户类型显示信息,显示商品逻辑(模型)结构,而不关心信息如何获得何时获得。

2.3 Controller

控制器接收用户的输入并调用模型和视图去完成用户的需求。(流程控制)

附录

参考:《阿里巴巴java开发手册》

专有名词

POJO( Plain Ordinary Java Object) : 在手册中, POJO 专指只有 setter / getter /toString 的简单类,包括 DO/DTO/BO/VO 等。GAV( GroupId、 ArtifactctId、 Version) : Maven 坐标,是用来唯一标识 jar 包。OOP( Object Oriented Programming) : 本手册泛指类、对象的编程处理方式。ORM( Object Relation Mapping) : 对象关系映射,对象领域模型与底层数据之间的转换,本文泛指 iBATIS, mybatis 等框架。NPE( java.lang.NullPointerException) : 空指针异常。SOA( Service-Oriented Architecture) : 面向服务架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用, 有利于提升组件可重用性,可维护性。IDE( Integrated Development Environment) : 用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具, 本《手册》 泛指 IntelliJ IDEA 和eclipse。OOM( Out Of Memory) : 源于 java.lang.OutOfMemoryError, 当 JVM 没有足够的内存来为对象分配空间并且垃圾回收器也无法回收空间时, 系统出现的严重状况。一方库: 本工程内部子项目模块依赖的库( jar 包) 。二方库: 公司内部发布到中央仓库,可供公司内部其它应用依赖的库( jar 包) 。三方库: 公司之外的开源库( jar 包) 。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。