Java面试技术点总结

前言

  前段时间换工作准备期间,总结了下在Android面试中易问的一些Java基础知识与面试题,不太全,大家查缺补漏吧。开始Review!
  

面向对象

Java面向对象编程思想

  将属性与功能封装起来,通过归类与抽象将相同或相似的部分抽出,不同的部分分离,将复杂业务逻辑切分成相互独立的部分,降低开发的难度。
  面向对象是解决了系统的可维护性,可扩展性和可重用性主要通过以下四大特性来实现:

  • 抽象核心思想!把相同的或相似的对象归为一类的这个过程就是抽象。抽象只关心对象中的主要问题,主要矛盾及相同的部分。它只在乎问题是什么,能够完成什么,而不在乎怎么去完成,交给他的实现类去解决。
  • 封装:将某些属性包装在一起,再以全新的形式呈现出来。其中隐藏属性,方法或实现的细节称之为封装。
  • 继承:重用现有的父类来生成新子类的一种特征,子类可通过继承父类来获取父类的属性与方法。
  • 多态:同一函数在不同的类中有不同的实现,使类变得更灵活,更便于扩充。父类引用指向子类对象。

设计模式

单例模式5种:

  • 懒汉式
  • 饿汉式
  • 同步线程锁懒汉式
  • 双重线程锁懒汉式
  • 最优单例

简单工厂模式

  由一个工厂类,根据传入参数的不同,动态创建不同的产品,这些产品都继承自一个父类或接口的实例。得益于Java的多态特性。

集合

List集合:有序,可重复

  • ArrayList:底层是动态数组顺序表数据结构,存储地址是连续的,所以查询速度快。但增删时需要移动其他元素的顺序,所以增删速度慢,线程不同步。
  • LinkedList:底层是双向链表数据结构,同时也实现了Deque双端队列接口,链表节点存储地址不连续。每个存储地址之间通过指正关联,查询时需要指针遍历所有节点,所以查询速度慢。而增删只需断开某元素前后的链接进行增加与删除即可,所以增删速度快,线程不同步。
  • Vector:类似ArrayList,线程同步,效率低。

Set集合:无序,不可重复

  • HashSet:底层数组,哈希表数据结构,线程不同步。
  • TreeSet:二叉树数据结构,可实现Comparable或Comparator接口来排序,线程不同步。

Map集合:键值对,键不可重复,值可以重复

  • HashMap:哈希表数据结构,可存入null键,null值,线程不同步。
  • HashTable:哈希表数据结构,不可存入null键,null值,线程同步。
  • TreeMap:二叉树数据结构,可进行排序,线程不同步

网络编程

TCP/IP层级

  • 应用层:向用户提供一些常用应用程序,例如电子邮件,文件传输访问等。包含常用协议HTTP(超文本传输协议:实现互联网中WWW服务)、DNS(域名解析:域名到IP地址间的转换)
  • 传输层:提供应用程序之前的传输通道。主要传输协议为TCP(面向连接传输控制协议,三次握手,安全可靠性高,适用于传输大量数据,但速度慢)与UDP(面向无连接用户数据报协议,无需握手同步,安全可靠性低,适用于传输小量数据,但速度快,例如QQ)。
  • 网络层:核心层,根据IP协议将分组装入IP数据报并发往目标网络或主机。主要传输协议为IP协议(用于源地址与目的地址间的传送数据报)与ICMP协议(传送IP控制信息)
  • 网络接口层:最底层,接收IP数据报,并通过网络发送

执行顺序

静态变量,静态代码块,本地变量执行顺序

  父类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class FatherTest {
static {
System.out.println("--父类的静态代码块--");
}
private String name;
{
System.out.println("--父类的非静态代码块--");
}
public FatherTest() {
System.out.println("--父类的无参构造函数--");
}
public FatherTest(String name) {
this.name = name;
System.out.println("--父类的有参构造函数--" + this.name);
}
public void show() {
System.out.println("--父类的show()方法--");
}
public static void main(String[] args) {
System.out.println("--父类的主程序--");
FatherTest fatherTest = new FatherTest("父亲的名字");
fatherTest.show();
}
}

  父类输出:

1
2
3
4
5
--父类的静态代码块--
--父类的主程序--
--父类的非静态代码块--
--父类的有参构造函数--父亲的名字
--父类的show()方法--

带继承关系的执行顺序**

  子类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class SonTest extends FatherTest {
static {
System.out.println("--子类的静态代码块--");
}
private String name;
{
System.out.println("--子类的非静态代码块--");
}
public SonTest() {
//默认super()调父类无参构造
System.out.println("--子类的无参构造函数--");
}
public SonTest(String name) {
//默认super()调父类无参构造
this.name = name;
System.out.println("--子类的有参构造函数--" + this.name);
}
@Override
public void show() {
//此处若不调用super.show()则只走子类重写的show()方法
System.out.println("--子类Override父类的show()方法--");
}
public static void main(String[] args) {
System.out.println("--子类的主程序--");
SonTest sonTest = new SonTest("儿子的名字");
sonTest.show();
}
}

  子类输出

1
2
3
4
5
6
7
8
--父类的静态代码块--
--子类的静态代码块--
--子类的主程序--
--父类的非静态代码块--
--父类的无参构造函数--
--子类的非静态代码块--
--子类的有参构造函数--儿子的名字
--子类Override父类的show()方法--

计算

Math的一些计算

  • Math.round():四舍五入,+0.5后向下取整(Math.round(11.5)=12,Math.round(-11.5)=-11),float返回int,double返回long
  • Math.rint():四舍五入,遇0.5则取偶数(Math.rint(11.4)=11.0,Math.rint(11.5)=12,Math.rint(10.5)=10)
  • Math.floor():不大于它的最大整数(Math.floor(11.5)=11,Math.floor(-11.5)=-12)
  • Math.ceil():不小于他的最小整数(Math.ceil(11.5)=12,Math.ceil(-11.5)=-11)
  • Math.abs():绝对值(Math.abs(11.5)=11.5,Math.abs(-11.5)=11.5)
  • Math.max():最大值(Math.max(-11.5, -11.4)=-11.4)
  • Math.min():最小值(Math.min(-11.5, -11.4)=-11.5)
  • Math.random():0.0-1.0之间的随机数
  • Math.pow(x, y):x的y次幂
  • Math.sqrt():开方

常量池计算

  Java的8种基本类型(Byte, Short, Integer, Long, Character, Boolean, Float, Double), 除Float和Double以外, 其它六种都实现了常量池, 但是它们只在大于等于-128并且小于等于127时才使用常量池.
  来看代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
System.out.println("等于127:");
System.out.println(a == b);
System.out.println("*****************");
a = 128;
b = 128;
System.out.println("等于128:");
System.out.println(a == b);
System.out.println("*****************");
a = -128;
b = -128;
System.out.println("等于-128:");
System.out.println(a == b);
System.out.println("*****************");
a = -129;
b = -129;
System.out.println("等于-129:");
System.out.println(a == b);
System.out.println("*****************");
// 测试Boolean
System.out.println("测试Boolean");
Boolean c = true;
Boolean d = true;
System.out.println(c == d);
d = new Boolean(true);
System.out.println(c == d);
}

  输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
等于127:
true
*****************
等于128:
false
*****************
等于-128:
true
*****************
等于-129:
false
*****************
测试Boolean
true
false

  结论:当我们给Integer赋值时,实际上调用了Integer.valueOf(int)方法,查看源码,其实现如下:

1
2
3
4
5
6
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}

  而IntegerCache实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static class IntegerCache {
static final int high;
static final Integer cache[];
static {
final int low = -128;
// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that
// require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue();
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
private IntegerCache() {}
}

  注意:cache数组是静态的。   

面试题

LinkedList工作原理与实现

  LinkedList是以双向链表实现,链表无容量限制(但是双向链表本身需要消耗额外的链表指针空间来操作),其内部主要成员为 first 和 last 两个 Node 节点,在每次修改列表时用来指引当前双向链表的首尾部位,所以 LinkedList 不仅仅实现了 List 接口,还实现了 Deque 双端队列接口(该接口是 Queue 队列的子接口),故 LinkedList 自动具备双端队列的特性,当我们使用下标方式调用列表的 get(index)、set(index, e) 方法时需要遍历链表将指针移动到位进行访问(会判断 index 是否大于链表长度的一半决定是首部遍历还是尾部遍历,访问的复杂度为 O(N/2)),无法像 ArrayList 那样进行随机访问。(如果i>数组大小的一半,会从末尾移起),只有在链表两头的操作(譬如 add()、addFirst()、removeLast() 或用在 iterator() 上的 remove() 操作)才不需要进行遍历寻找定位。

使用LinkedList模拟一个堆栈或队列的数据结构 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Queue {
private LinkedList link;
public Queue() {
link = new LinkedList();
}
public void push(Object obj) {
link.addLast(obj);
}
public Object pop() {
//堆栈先进后出,队列先进先出
return link.removeFirst(); //队列
//return link.removeLast(); //堆栈
}
public boolean isEmpty() {
return link.isEmpty();
}
}

ConcurrentModificationException异常

  计算下面代码的输出结果:

1
2
3
4
5
6
7
8
9
10
11
List<String> list = new ArrayList<>();
list.add("di");
String str = new String("da");
list.add(str);
list.add("paint");
for(String temp : list){
if(temp == "di" || temp == "da"){
list.remove(temp);
}
}
System.out.print(list.toString());

  • 解答:会抛出ConcurrentModificationException异常,List不可在进行遍历时添加或移除其中的元素。
  • 源码解析:ArrayList的父类AbstractList中有一个modCount成员变量来记录对List的修改次数,还有一个expectedModCount记录对ArrayList修改次数的期望值,modCount是它的初始值。遍历开始时modCount与expectedModCount都为0,而当我们在遍历中对List进行了add或remove操作后modCount会增加1,expectedModCount不变。而List在通过next()方法取下一次值时会先检查modCount与expectedModCount是否相等,不等则会抛出ConcurrentModificationException异常。
  • 单线程中解决方案:使用Iterator遍历集合,调用Iterator的remove方法移除元素,其内部有expectedModCount = modCount的操作。
  • 多线程中解决方案:多线程中使用上述方法也会抛出此异常,可在使用iterator迭代的时候使用synchronized或者Lock进行同步,或使用CopyOnWriteArrayList替换ArrayList。

通过代码输出365的二进制

  • 简单版
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
int a = 365;//定义一个变量并赋给他一个十进制的值
int remainder;//定义一个变量用于存储余数
int sum = 0;//定义一个变量用于存放和
int k = 1;//定义一个变量控制位数
while(a != 0){
remainder = a % 2;//对目标数字求余
a /= 2;//对目标数字求商
sum = sum + remainder * k;//求和
k *= 10;//改变位数
}
System.out.println("10进制的365转换为2进制结果为:" + sum );
}
  • 负数版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String[] args) {
int n = -10;
String result = "";
boolean minus = false;
//如果该数字为负数,那么进行该负数+1之后的绝对值的二进制码的对应位取反,然后将它保存在result结果中
if(n < 0){
minus = true;
n = Math.abs(n + 1);
}
while(true){
int remainder = (!minus && n % 2 == 0) || (minus && n % 2 == 1) ? 0 : 1;
//将余数保存在结果中
result = remainder + result;
n /= 2;
if(n == 0){
break;
}
}
//判断是否为负数,如果是负数,那么前面所有位补1
if(minus){
n = result.length();
for(int i = 1; i <= 32 - n; i++){
result = 1 + result;
}
}
System.out.println(result);
}

总结

  目前Java面试技术点总结的还不太全,我会持续更新,也欢迎码友们指出文中写的不对的地方,或者有遗漏的知识点也可以联系我,咱们一起来维护,方便你我他。最后祝各位求职顺利,入职满意的公司。

坚持原创技术分享,您的支持是我前进的动力,谢谢!