Java面试总结

Java语言

八种基本数据类型及其包装类

整型:byte,short,int,long
浮点型:float,double
逻辑型:boolean
字符型:char

原始类型原始类型所占的字节数包装类
byte1个字节Byte
shot2个字节Short
int4个字节Integer
long8个字节Long
float4个字节Float
double8个字节Double
boolean1个字节Boolean
char2个字节Character

要注意的是基本数据的包装类很多都实现了享元模式。享元模式就是运用共享技术有效地支持大量细粒度对象的复用。用一个常见的面试题来解释

1.判断如下代码的输出,并说出原因

Integer a1 = 40;
Integer a2 = 40;
System.out.println(a1 == a2);

Integer a3 = 200;
Integer a4 = 200;
System.out.println(a3 == a4);

由自动装箱和拆箱可以知道这2种写法是等价的

Integer a1 = 40;
Integer a1 = Integer.valueOf(40);

看一下Integer的valueOf方法

public static Integer valueOf(int i) {
    // i的取值范围为[-128,127]
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

IntegerCache是Ingeter的静态内部类,默认创建了[-128,127]的对象,并放到IntegerCache内部的一个cache数组中,在[-128,127]这个范围内的整数对象,不用创建。直接从IntegerCache中的cache数组中根据下标拿就可以,超出这个范围的每次去创建新的对象。其他几种包装类型的常量池和Integer思路都差不多,源码都很相似。

所以答案如下:

Integer a1 = 40;
Integer a2 = 40;

// true
System.out.println(a1 == a2);

Integer a3 = 200;
Integer a4 = 200;
// false
System.out.println(a3 == a4);

包装类缓存的范围如下

包装类缓存范围
Byte-128~127
Short-128~127
Integer-128~127
Long-128~127
Character0~127

2.Java一个char类型可以存储中文吗?
可以,因为Java中使用了Unicode字符,不论中文还是因为固定占用2个字节。

char a = '中';
// 中
System.out.println(a);

3.什么是自动装箱,自动拆箱?
自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱

自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值

// jdk1.5 之前的写法
Integer tempNum1 = Integer.valueOf(5);
int num1 = tempNum1.intValue();

// jdk1.5之后的写法
Integer tempNum2 = 5;
int num2 = tempNum2;

4.为什么要需要基本数据类型的包装类?
(1)Java是面向对象的语言,很多地方需要使用的是对象而不是基本数据类型。例如,List,Map等容器类中基本 数据类型是放不进去的。
(2)包装类在原先的基本数据类型上,新增加了很多方法,如Integer.valueOf(String s)等

5.既然包装类型能完成所有功能,为啥还需要基本类型?
基本数据类型基于数值,对象类型基于引用。基本数据类型存储在栈的局部变量表中。
而对象类型的变量则存储堆中引用,实例放在堆中,因此对象类型的变量需要占用更多的内存空间。

显然,相对于基本类型的变量来说,对象类型的变量需要占用更多的内存空间。

5.写出如下代码的输出

Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);

System.out.println(i1 == i2);
System.out.println(i1 == i2 + i3);
System.out.println(i1 == i4);
System.out.println(i4 == i5);
System.out.println(i4 == i5 + i6);
System.out.println(40 == i5 + i6);

输入如下

Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);

// true
// Integer.valueOf()用了常量池,看上面的源码
System.out.println(i1 == i2);

// true
// + 操作会导致左右2边都转成基本数据类型
// 具体原因看下面
System.out.println(i1 == i2 + i3);

// false
// Integer.valueOf()使用常量池中的对象
// new Integer每次会创建新对象,
System.out.println(i1 == i4);

// false
// 2个不同的对象
System.out.println(i4 == i5);

// true、
// 解释在最下面
System.out.println(i4 == i5 + i6);

// true
// 解释在最下面
System.out.println(40 == i5 + i6);

语句i4 == i5 + i6,因为+这个操作符不适用于Integer对象,首先i5和i6进行自动拆箱操作,进行数值相加,即i4 == 40。然后Integer对象无法与数值进行直接比较,所以i4自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较

抽象类和接口

接口和抽象类的相似性

  1. 都不能被实例化
  2. 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法

接口和抽象类的区别

  1. 接口只能包含抽象方法,静态方法和默认方法,抽象类则可以包含普通方法
  2. 接口里只能定义静态常量,不能定义普通成员变量,抽象类里既可以定义普通成员变量,也可以定义静态常量
  3. 接口不能包含构造器,抽象类可以包含构造器。抽象类的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作
  4. 接口里不能包含初始化快,抽象类里可以包含初始化块
  5. 一个类最多只能有一个直接父类,包括抽象类。但一个类可以实现多个接口

StringBuffer和StringBuilder的区别

先来看String类的实现

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}

先来说一下final关键字的作用

1.final修饰类时,表明这个类不能被继承
2.final修饰方法,表明方法不能被重写
3.final修饰变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象

可以看到String类和保存变量的value数组都被final修饰,表明String类是不可变的。
StringBuffer和StringBuilder都继承自AbstractStringBuilder类,看一下AbstractStringBuilder类的定义

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;
}

看到区别了吗?value数组没有用private和final修饰,说明了StringBuffer和StringBuilder是可变的。

而StringBuilder和StringBuffer的方法是差不多的,只不过StringBuffer在方法上添加了
synchronized关键字,所以在多线程环境下我们要用StringBuffer来保证线程安全,单线程环境下用StringBuilder来获得更高的效率。

看2个类中同一个方法的定义

// StringBuffer

@Override
public synchronized StringBuffer append(char[] str) {
	toStringCache = null;
	super.append(str);
	return this;
}
// StringBuilder 

@Override
public StringBuilder append(char[] str) {
	super.append(str);
	return this;
}

因为StringBuffer和StringBuilder的实现类似,所以性能比较就落在String和StringBuilder之间了。

1.String是不可变对象,每次操作都会生成新的String对象,然后将指针指向新的对象。
2.抽象类AbstractStringBuilder内部提供了一个自动扩容机制,当发现长度不够的时候,会自动进行扩容工作(具体扩容可以看源码,很容易理解),会创建一个新的数组,并将原来数组的数据复制到新数组,不会创建新对象,拼接字符串的效率高。

用源码证实一下

// String

public String substring(int beginIndex) {
	if (beginIndex < 0) {
		throw new StringIndexOutOfBoundsException(beginIndex);
	}
	int subLen = value.length - beginIndex;
	if (subLen < 0) {
		throw new StringIndexOutOfBoundsException(subLen);
	}
	return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
// StringBuilder

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

介绍完毕,所以你应该知道这道题应该怎么答了

常见面试题

1.说一下String StringBuffer StringBuilder的区别?

  1. 都是final类,不允许被继承
  2. String长度是不可变的,StringBuffer,StringBuilder长度是可变的
  3. StringBuffer是线程安全的,StringBuilder不是线程安全的。但它们方法实现类似,StringBuffer在方法之上添加了synchronized修饰,保证线程安全
  4. StringBuilder比StringBuffer拥有更好的性能
  5. 如果一个String类型的字符串,在编译时可以确定是一个字符串常量,则编译完成之后,字符串会自动拼接成一个常量,此时String的速度比StringBuffer和StringBuilder的性能好的多
  6. 字符串拼接速度StringBuilder>StringBuffer>String

我用例子解释一下第五条

public static void main(String[] args) {
    String a = "a";
    String b = "b";
    String c = a + b;
    String d = "a" + "b" + "c";
}

反编译class文件后是这样的

public static void main(String[] args) {
	String a = "a";
	String b = "b";
	(new StringBuilder()).append(a).append(b).toString();
	String d = "abc";
}

看string d,理解了吗?
同时看string c的拼接过程,先生成一个StringBuilder对象,再调用2次append方法,最后再返回一个String对象,知道String比StringBuilder慢的原因了吧

算法和数据结构

二叉树

定义

二叉树是一种树形结构,它的特点是每个节点至多只有两颗子树(即二叉树中不存在度大于2的几点),并且,二叉树的子树有左右之分,其次序不能任意颠倒

性质

  1. 在二叉树的第i层上至多有 2 i − 1 2^i-1 2i1个节点(i>=1)
  2. 深度为k的二叉树至多有 2 k − 1 2^k-1 2k1个节点(k>=1)
  3. 对任何一颗二叉树T,如果其终端节点数为 n 0 n_0 n0,度为2的节点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
  4. 具有n个节点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n \rfloor+1 log2n+1

遍历二叉树

先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
记忆方法:左右的位置不变,先序遍历根在最前面,中序遍历根在中间,同理,后序遍历根就在最后面了

排序

冒泡排序

public class BubbleSort {

    //交换元素顺序
    public static void swap(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    //冒泡排序
    public static void bubbleSort(int[] a) {
        for (int i=0; i<a.length - 1; i++) {
            for (int j=0; j<a.length - 1 - i; j++) {
                if (a[i] > a[i + 1]) {
                    swap(a, i, i + 1);
                }
            }
        }
    }

    public static void main(String[] args) {

        int[] a = {1, 5, 2, 4, 7, 6};
        bubbleSort(a);
        //[1, 2, 4, 5, 6, 7]
        System.out.println(Arrays.toString(a));
    }
}

选择排序

public class SelectSort {

    public static void swap(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void selectSort(int[] a) {
        for (int i=0; i<a.length; i++) {
            int min = a[i];
            int index = i;
            for (int j=i; j<a.length; j++) {
                if (a[j] < min) {
                    min = a[j];
                    index = j;
                }
            }
            if (index != i) {
                swap(a, i, index);
            }
        }
    }
    public static void main(String[] args) {
        int[] a = {1, 5, 2, 4, 7, 6};
        selectSort(a);
        //[1, 2, 4, 5, 6, 7]
        System.out.println(Arrays.toString(a));
    }
}

快速排序

  1. 从数列中取出一个数作为基准数
  2. 分区过程,将比它大的数全放到它的右边,小于或等于它的数全放到它的左边
  3. 再对左右区间重复第二步,直到各区间只有一个数
public class QuickSort {


    public static int sort(int[] a, int low, int high) {
        int key = a[low];
        while (low < high) {
            //从high所指位置向前搜索找到第一个关键字小于key的记录和key互相交换
            while (low < high && a[high] >= key) {
                high--;
            }
            a[low] = a[high];
            //从low所指位置向后搜索,找到第一个关键字大于key的记录和key互相交换
            while (low < high && a[low] <= key) {
                low++;
            }
            a[high] = a[low];
        }
        //此时low和key相等
        a[low] = key;
        return low;
    }

    public static void quickSort(int[] a, int low, int high) {
        if (low < high) {
            int key = sort(a, low, high);
            quickSort(a, low, key - 1);
            quickSort(a, key + 1, high);
        }
    }

    public static void main(String[] args) {
        int[] a = {1, 5, 2, 4, 7, 6};
        quickSort(a, 0, a.length - 1);
        //[1, 2, 4, 5, 6, 7]
        System.out.println(Arrays.toString(a));
    }

}

归并排序

假设初始序列含有n个记录,则可看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到$ \lceil \frac{x}{2} \rceil $个长度为2或1的有序子序列,在两两归并,…,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2-路归并排序
这里写图片描述

public class MergeSort {

    public static void sort(int[] src) {
        int[] temp =  new int[src.length];
        msort(src,temp,0,src.length-1);
    }

    public static void msort(int[] src,int[] dest,int left,int right) {

        if (left < right) {
            int mid = (left + right) / 2;
            msort(src,dest,0,mid);
            msort(src,dest,mid+1,right);
            merge(src,dest,0,mid,right);
        }
    }

    public static void merge(int[] src,int[] dest,int left,int mid,int right) {
        int i = left;//左边数组的游标
        int j = mid + 1;//右边数组的游标
        int index = 0;//dest起一个中途存储的作用,这个是dest数组的游标
        while (i <= mid && j <= right) {
            if (src[i] <= src[j]) {
                dest[index++] = src[i++];
            } else {
                dest[index++] = src[j++];
            }
        }

        //复制左边剩余的数组
        while (i <= mid) {
            dest[index++] = src[i++];
        }
        //复制右边剩余的数组
        while (j <= right) {
            dest[index++] = src[j++];
        }
        index = 0;
        while (left <= right) {
            src[left++] = dest[index++];
        }
    }
    
    public static void main(String[] args) {

        int[] arr = {7,5,3,4,2,1,6,2,9,8};
        sort(arr);
        //[1, 2, 2, 3, 4, 5, 6, 7, 8, 9]
        System.out.println(Arrays.toString(arr));
    }
}

二分查找

public class Search {


    public static void main(String[] args) {
        int test1[] = {3,4,5,1,2,7,9};
        //false
        System.out.println(binarySearch(test1, 10));
        //true
        System.out.println(binarySearch(test1, 9));
    }

    //从array数组中查找target的值,找到返回true,否则返回false
    public static boolean binarySearch(int[] array, int target) {

        //进行二分查找先进行排序
        Arrays.sort(array);
        int left = 0;
        int right = array.length -1;
        //注意是小于等于,如从123456中查找6,没等于不行
        while (left <= right) {
            int mid = (left + right) >> 1;
            if (target == array[mid]) {
                 return true;
            } else if (target > array[mid]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return false;
    }
}

深度优先搜索

给定图G的初始状态是所有顶点均未曾访问过,在G中任选一顶点v为初始出发点(源点或根节点),则深度优先遍历可定义如下:首先访问出发点v,并将其标记为已访问过,然后从v出发搜索v的每个未曾访问的的邻节点w,并以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点均被访问为止。若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止
##广度优先搜索
设图G的初始状态是所有顶点均未访问过。以G中任选一顶点v为起点,则广度优先搜索定义为:首先访问出发点v,接着依次访问v的所有邻接点 w 1 w_1 w1 w 2 w_2 w2 w 3 w_3 w3,…, w i w_i wi,然后再依次访问与 w 1 w_1 w1 w 2 w_2 w2 w 3 w_3 w3,…, w i w_i wi邻接的所有未曾访问过的顶点。以此类推,直至图中所有和起点v有路径相通的顶点都已访问到为止。此时从v开始的搜索过程结束。
若G是连通图,则一次就能搜索完所有节点;否则,在图G中另选一个尚未访问的顶点作为新源点继续上述搜索过程,直至G中所有顶点均已被访问为止

用户点击页面到收到结果中间发生了什么,从servlet的角度

  1. 用户点击页面发送查询请求->Web服务器应用(如Apache)->Web容器应用(如tomcat)
  2. 容器创建两个对象HttpServletRequest和HttpServletResponse
  3. 根据URL找到servlet,并为请求创建或分配一个线程,将请求和响应对象传递给这个servlet线程
  4. 容器调用Servlet的service()方法,根据请求的不同类型,service()方法会调用doGet()和doPost()方法,假如请求是HTTP POST请求
  5. doPost()查询数据库获得数据,并把数据增加到请求对象
  6. servlet把请求转发给jsp,jsp为容器生成页面
  7. 线程结束,容器把响应对象装换为一个HTTP请求,把它发回给客户,然后删除请求和响应对象

MySQL索引优化策略

https://blog.csdn.net/zzti_erlie/article/details/87898784

数据库事务的四个特性

https://blog.csdn.net/zzti_erlie/article/details/81094178

MySQL为什么要用B+树实现

https://blog.csdn.net/zzti_erlie/article/details/82973742

voliate

线程池

HashTable和ConcurrentHashMap的区别

Lock

CountDownLatch

Spring MVC执行流程

单例模式的5种写法

https://blog.csdn.net/zzti_erlie/article/details/80714424

Spring AOP和IOC

spring ioc的用处

nio

netty

jvm

try catch

求出数据库中重复的记录

参考博客

快速排序
[1]http://blog.csdn.net/morewindows/article/details/6684558
Stringbuilder和Stringbuffer
[2]https://www.cnblogs.com/su-feng/p/6659064.html
Java和C++的区别
[3]http://blog.csdn.net/shennongzhaizhu/article/details/51897060

  • 35
    点赞
  • 191
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java识堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值