程序员技术干货java

面试Java开发常用题的答案

2018-11-04  本文已影响12人  萧萧笔记

一、数据结构与算法基础

· 说一下几种常见的排序算法和分别的复杂度。

· 用Java写一个冒泡排序算法

/**
现在有一个包含1000个数的数组,仅前面100个无序,后面900个都已排好序且都大于前面100个数字,那么在第一趟遍历后,最后发生交换的位置必定小于100,且这个位置之后的数据必定已经有序了,也就是这个位置以后的数据不需要再排序了,于是记录下这位置,第二次只要从数组头部遍历到这个位置就可以了。如果是对于上面的冒泡排序算法2来说,虽然也只排序100次,但是前面的100次排序每次都要对后面的900个数据进行比较,而对于现在的排序算法3,只需要有一次比较后面的900个数据,之后就会设置尾边界,保证后面的900个数据不再被排序。
*/
public static void bubbleSort(int [] a, int n){
    int j , k;
    int flag = n ;//flag来记录最后交换的位置,也就是排序的尾边界

    while (flag > 0){//排序未结束标志
        k = flag; //k 来记录遍历的尾边界
        flag = 0;

        for(j=1; j<k; j++){
            if(a[j-1] > a[j]){//前面的数字大于后面的数字就交换
                //交换a[j-1]和a[j]
                int temp;
                temp = a[j-1];
                a[j-1] = a[j];
                a[j]=temp;

                //表示交换过数据;
                flag = j;//记录最新的尾边界.
            }
        }
    }
}

· 描述一下链式存储结构。

它不要求逻辑上相邻的元素在物理位置上也相邻。因此它没有顺序存储结构所具有的弱点,同时也失去了顺序表可随机存取的优点。
其特点主要表现为:
1、比顺序存储结构的存储密度小;
2、插入、删除灵活,结点可以被插入到链表的任何位置,首、中、末都可以,而且不必要移动结点中的指针;
3、链表的大小可以按需伸缩,是一种动态存储结构,其实现的集合在增、删方面性能更高;
4、查找结点时的效率就相对数组较低,只能从第一个结点开始顺着链表逐个查找(这是他的缺点)。

· 如何遍历一棵二叉树?

二叉树的遍历分为三种:
前序遍历:按照“根左右”,先遍历根节点,再遍历左子树 ,再遍历右子树
中序遍历:按照“左根右“,先遍历左子树,再遍历根节点,最后遍历右子树
后续遍历:按照“左右根”,先遍历左子树,再遍历右子树,最后遍历根节点
其中前,后,中指的是每次遍历时候的根节点被遍历的顺序

package com.tree;

import java.util.ArrayList;
import java.util.List;

public class Tree {
    private Node root;
    private List<Node> list=new ArrayList<Node>();
    public Tree(){
        init();
    }
    //树的初始化:先从叶节点开始,由叶到根
    public void init(){
        Node x=new Node("X",null,null);
        Node y=new Node("Y",null,null);
        Node d=new Node("d",x,y);
        Node e=new Node("e",null,null);
        Node f=new Node("f",null,null);
        Node c=new Node("c",e,f);
        Node b=new Node("b",d,null);
        Node a=new Node("a",b,c);
        root =a;
    }
    //定义节点类:
    private class Node{
      private String data;
      private Node lchid;//定义指向左子树的指针
      private Node rchild;//定义指向右子树的指针
      public Node(String data,Node lchild,Node rchild){
          this.data=data;
          this.lchid=lchild;
          this.rchild=rchild;
      }
    }

    /**
      * 对该二叉树进行前序遍历 结果存储到list中 前序遍历:ABDXYCEF
      */
     public void preOrder(Node node)
     {

            list.add(node); //先将根节点存入list
            //如果左子树不为空继续往左找,在递归调用方法的时候一直会将子树的根存入list,这就做到了先遍历根节点
            if(node.lchid != null)
            {
                preOrder(node.lchid);
            }
            //无论走到哪一层,只要当前节点左子树为空,那么就可以在右子树上遍历,保证了根左右的遍历顺序
            if(node.rchild != null)
            {
                preOrder(node.rchild);
            }
     }

     /**
      * 对该二叉树进行中序遍历 结果存储到list中
      */
     public void inOrder(Node node)
     {
        if(node.lchid!=null){
            inOrder(node.lchid);
        }
        list.add(node);
        if(node.rchild!=null){
            inOrder(node.rchild);
        }
     }

     /**
      * 对该二叉树进行后序遍历 结果存储到list中
      */
     public void postOrder(Node node)
     {
         if(node.lchid!=null){
             postOrder(node.lchid);
         }
         if(node.rchild!=null){
             postOrder(node.rchild);
         }
         list.add(node);

     }

     /**
      * 返回当前数的深度
      *  说明:
      *  1、如果一棵树只有一个结点,它的深度为1。
      *  2、如果根结点只有左子树而没有右子树,那么树的深度是其左子树的深度加1;
      *  3、如果根结点只有右子树而没有左子树,那么树的深度应该是其右子树的深度加1;
      *  4、如果既有右子树又有左子树,那该树的深度就是其左、右子树深度的较大值再加1。
      *  
      * @return
      */
     public int getTreeDepth(Node node) {

            if(node.lchid == null && node.rchild == null)
            {
                return 1;
            }
            int left=0,right = 0;
            if(node.lchid!=null)
            {
                left = getTreeDepth(node.lchid);
            }
            if(node.rchild!=null)
            {
                right = getTreeDepth(node.rchild);
            }
            return left>right?left+1:right+1;
        }


    //得到遍历结果
     public List<Node> getResult()
     {
      return list;
     }

     public static void main(String[] args) {
        Tree tree=new Tree();
        System.out.println("根节点是:"+tree.root);
        //tree.preOrder(tree.root);
        tree.postOrder(tree.root);
        for(Node node:tree.getResult()){
            System.out.println(node.data);
        }
        System.out.println("树的深度是"+tree.getTreeDepth(tree.root));

    } 
}

二叉树与一般树的区别

  • 一般树的子树不分次序,而二叉树的子树有左右之分.
  • 由于二叉树也是树的一种,所以大部分的树的概念,对二叉树也适用.
  • 二叉树的存贮:每个节点只需要两个指针域(左节点,右节点),有的为了操作方便也会 增加指向父级节点的指针,除了指针域以外,还会有一个数据域用来保存当前节点的信息

二叉树的特点:

  • 性质1:在二叉树的第i层上至多有2^(i-1)个节点(i >= 1)
  • 性质2:深度为k的二叉树至多有2^k-1个节点(k >=1)
  • 性质3:对于任意一棵二叉树T而言,其叶子节点数目为N0,度为2的节点数目为N2,则有N0 = N2 + 1。
  • 性质4:具有n个节点的完全二叉树的深度。

· 倒排一个LinkedList。

根据LinkedList的实现,LinkedList的底层是双向链表,它在get任何一个位置的数据的时候,都会把前面的数据走一遍。用迭代器或者foreach循环(foreach循环的原理就是迭代器)去遍历LinkedList即可,这种方式是直接按照地址去找数据的,将会大大提升遍历LinkedList的效率。

public static <T> LinkedList<T> reverse(LinkedList<T> linkedList)
{
    if (linkedList == null) return linkedList;
    LinkedList<T> temp_linkedlist = new LinkedList<T>();
    for (T item: linkedList) {
        temp_linkedlist.addLast(item);
    }
    return temp_linkedlist;
}

· 用Java写一个遍历目录下面的所有文件。

public static void foreachFileList(String filePath) throws IOException {
    LinkedList<File> linkedList = new LinkedList<File>();
    File file = new File(filePath);
    if (file.exists()) {
        linkedList.add(file);
        while (true) {
            file = linkedList.poll();
            if (file == null) break;

            File[] fileList = file.listFiles();
            for (File fileItem : fileList) {
                if (fileItem.isDirectory()) {
                    linkedList.add(fileItem);
                    continue;//for
                }
                if (fileItem.isFile())
                    System.out.println(fileItem.getCanonicalPath());
            }
        }
    }
}

二、Java基础

· 接口与抽象类的区别?

· Java中的异常有哪几类?分别怎么使用?

· 常用的集合类有哪些?比如List如何排序?

· ArrayList和LinkedList内部的实现大致是怎样的?他们之间的区别和优缺点?

· 内存溢出是怎么回事?请举一个例子?

· ==和equals的区别?

· hashCode方法的作用?

  1. hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;
  2. 如果两个对象相同,就是适用于equals(Java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;
  3. 如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;
  4. 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。

· NIO是什么?适用于何种场景?

· HashMap实现原理,如何保证HashMap的线程安全?

HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。HashMap底层就是一个数组结构,数组中的每一项又是一个链表。

· JVM内存结构,为什么需要GC?

垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存,简化代码开发。

· NIO模型,select/epoll的区别,多路复用的原理

(过长,没有回答)

· Java中一个字符占多少个字节,扩展再问int, long, double占多少字节

Java char: utf-16:2个字节, int-4, long-8,double-9

· 创建一个类的实例都有哪些办法?

  1. 关键字 new。工厂模式是对这种方式的包装;
  2. 类实现克隆接口,克隆一个实例。原型模式是一个应用实例;
  3. 用该类的加载器,newinstance。java的反射,反射使用实例:Spring的依赖注入、切面编程中动态代理等取得
  4. 通过IO流反序列化读取一个类,获得实例。

· final/finally/finalize的区别?

  1. final:如果一个类被final修饰,意味着该类不能派生出新的子类,不能作为父类被继承。因此一个类不能被声明为abstract,又被声明为final。将变量或方法声明为final。可以保证他们在使用的时候不被改变。其初始化可以在两个地方:一是其定义的地方,也就是在final变量在定义的时候就对其赋值;二是在构造函数中。这两个地方只能选其中的一个。被声明为final的方法也只能使用,不能重写。
  2. finally:在异常处理的时候,提供finally块在成功或失败都可以执行任何的清除操作。
  3. finalize:finalize是方法名,java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的,可以从Object.finalize()继承下来。

· Session/Cookie的区别?

· String/StringBuffer/StringBuilder的区别,扩展再问他们的实现?

StringBuilder:线程非安全的,StringBuffer:线程安全的,三者在执行速度方面的比较:StringBuilder > StringBuffer > String

· Servlet的生命周期?

  1. Servlet 通过调用 init () 方法进行初始化。
  2. Servlet 调用 service() 方法来处理客户端的请求。
  3. Servlet 通过调用 destroy() 方法终止(结束)。
  4. 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

· 如何用Java分配一段连续的1G的内存空间?需要注意些什么?

使用ArrayList来分配,注意堆内存不足造面OOM

· Java有自己的内存回收机制,但为什么还存在内存泄漏的问题呢?

主要是没有释放对象引用造成的内存泄漏,比如下例:

class MyList{
    /* 
     * 此处只为掩饰效果,并没有进行封装之类的操作
     * 将List集合用关键字 static 声明,这时这个集合将不属于任MyList 对象,而是一个类成员变量
     */  
    public static List<String> list = new ArrayList<String>();
}  
  
 class Demo{  
     public static void main(String[] args) {  
         MyList list = new MyList();  
         list.list.add("123456");  
         // 此时即便我们将 list指向null,仍然存在内存泄漏,因为MyList中的list是静态的,它属于类所有而不属于任何特定的实例  
         list = null;  
    }  
 }  

· 什么是java序列化,如何实现java序列化?(写一个实例)?

序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。Java的序列化需要实现Serializable接口。

//序列化后生成指定文件路径 
File file = new File("D:" + File.separator + "person.ser") ;
ObjectOutputStream oos = null ;
//装饰流(流)
oos = new ObjectOutputStream(new FileOutputStream(file)) ; 
//实例化类 
Person per = new Person("张三",30) ;
oos.writeObject(per) ;
//把类对象序列化
oos.close() ;

· String s = new String("abc");创建了几个 String Object?

两个对象,一个是“abc”对象,在常量池中创建;一个是new关键字创建的s对象指向“abc”。

三、JVM

· JVM堆的基本结构。

java_heap_struct.jpg

参考阅读:JVM内存堆布局图解分析

· JVM的垃圾算法有哪几种?CMS垃圾回收的基本流程?

基本的算法有:

  1. 标记-清除算法

    等待被回收对象在被标记后直接对对象进行清除,会带来另一个新的问题——内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。

  2. 复制算法(Java堆中新生代的垃圾回收算法)

    此GC算法实际上解决了标记-清除算法带来的“内存碎片化”问题。首先还是先标记处待回收内存和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域则是连续的。它的缺点就是会损失掉部分系统内存,因为你总要腾出一部分内存用于复制。

  3. 标记-压缩算法(或称为标记-整理算法,Java堆中老年代的垃圾回收算法)

    对于新生代,大部分对象都不会存活,所以在新生代中使用复制算法较为高效,而对于老年代来讲,大部分对象可能会继续存活下去,如果此时还是利用复制算法,效率则会降低。标记-压缩算法首先还是“标记”,标记过后,将不用回收的内存对象压缩到内存一端,此时即可直接清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。老年代的垃圾回收称为“Major GC”。

    jvm_gc.png

CMS的基本流程:

  1. 初始标记 :在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
  2. 并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
  3. 并发预清理 :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。
  4. 重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。
  5. 并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
  6. 并发重置 :这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收

延伸说明:

· JVM有哪些常用启动参数可以调整,描述几个?

各主要JVM启动参数的作用如下:
-Xms:设置jvm内存的初始大小
-Xmx:设置jvm内存的最大值
-Xmn:设置新域的大小(这个似乎只对jdk1.4来说是有效的,后来就废弃了)
-Xss:设置每个线程的堆栈大小(也就是说,在相同物理内存下,减小这个值能生成更多的线程)
-XX:NewRatio:设置新域与旧域之比,如-XX:NewRatio=4就表示新域与旧域之比为1:4
-XX:NewSize:设置新域的初始值
-XX:MaxNewSize:设置新域的最大值
-XX:MaxPermSize:设置永久域的最大值
-XX:SurvivorRatio=n:设置新域中Eden区与两个Survivor区的比值。(Eden区主要是用来存放新生的对象,而两个Survivor区则用来存放每次垃圾回收后存活下来的对象)
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimestamps -XX:+PrintGCApplicationStopedTime
JVM启动参数使用中常见的错误:
java.lang.OutOfMemoryError相信很多开发人员都用到过,这个主要就是JVM参数没有配好引起的,但是这种错误又分两种:java.lang.OutOfMemoryError:Javaheapspace和java.lang.OutOfMemoryError:PermGenspace,其中前者是有关堆内存的内存溢出,可以同过配置-Xms和-Xmx参数来设置,而后者是有关永久域的内存溢出,可以通过配置-XX:MaxPermSize来设置。

· 如何查看JVM的内存使用情况?

jstat,jmap,jstack,j viusalvm

· Java程序是否会内存溢出,内存泄露情况发生?举几个例子。

  1. 静态集合类引起内存泄漏:
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
    Object o = new Object();
    v.add(o);
    o = null;
}
//o的对象还在Vector中,因此需要从中移除,或者直接把vector=null
  1. 各种资源性连接没有正确关闭.比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。
  2. 不正确使用单例模式是引起内存泄漏的一个常见问题.
  3. 当集合里面的对象属性被修改后,造成该对象的Hashcode改变了,再调用remove()方法时不起作用。

· 你常用的JVM配置和调优参数都有哪些?分别什么作用?

  1. Trace跟踪参数(跟踪GC、类、变量的内存变化情况)
  1. 堆的分频参数
  1. 栈的分配参数
    -Xss 每个线程都有独立的栈空间(几百k,比较小)需要大量线程时,需要尽可能减小栈空间
    栈空间太小-----StackOverFlow栈溢出(一般递归时产生大量局部变量导致)

    总结:
    1.根据实际情况调整新生代和幸存代的大小
    2.官方推荐:新生代占堆空间3/8
    3.幸存代占新生代1/10
    4.OOM时,dump出堆到文件,方便排查

· 常用的GC策略,什么时候会触发YGC,什么时候触发FGC?

常见内存回收策略:

  1. 串行&并行

    串行:单线程执行内存回收工作。十分简单,无需考虑同步等问题,但耗时较长,不适合多cpu。

    并行:多线程并发进行回收工作。适合多CPU,效率高。
  2. 并发& stop the world

    stop the world:jvm里的应用线程会挂起,只有垃圾回收线程在工作进行垃圾清理工作。简单,无需考虑回收不干净等问题。

    并发:在垃圾回收的同时,应用也在跑。保证应用的响应时间。会存在回收不干净需要二次回收的情况。
  3. 压缩&非压缩&copy
    压缩:在进行垃圾回收后,会通过滑动,把存活对象滑动到连续的空间里,清理碎片,保证剩余的空间是连续的。
    非压缩:保留碎片,不进行压缩。
    copy:将存活对象移到新空间,老空间全部释放。(需要较大的内存。)

四、多线程/并发

· 如何创建线程?如何保证线程安全?

保证线程安全: 竞争与原子操作、同步与锁、可重入、使用valatile防止CPU过度优化。

  1. 继承Thread类,必须重写run方法,在run方法中定义需要执行的任务。
  2. 实现Runnable接口。
    MyRunnable runnable = new MyRunnable();
    Thread thread = new Thread(runnable);
    thread.start();
    class MyRunnable implements Runnable{... }

Note: Java如何创建进程:
第一种方式是通过Runtime.exec()方法来创建一个进程,第二种方法是通过ProcessBuilder的start方法来创建进程。

· 什么是死锁?如何避免死锁

· Volatile关键字的作用?

volatile特性一:内存可见性,即线程A对volatile变量的修改,其他线程获取的volatile变量都是最新的。volatile特性二:可以禁止指令重排序,避免重排序指令后访问数据错乱.
Volatile可以看做一种轻量级的锁,但又和锁有些不同。
a) 它对于多线程,不是一种互斥(mutex)关系。
b) 用volatile修饰的变量,不能保证该变量状态的改变对于其他线程来说是一种“原子化操作”。

· HashMap在多线程环境下使用需要注意什么?为什么?

· Java程序中启动一个线程是用run还是start?

start方法:通过该方法启动线程的同时也创建了一个线程,真正实现了多线程。而run方法只是当初普通的方法调用.

· 什么是守护线程?有什么用?

守护线程(即daemon thread)是服务线程,处理后台事务,如垃圾回收等,JVM内部的实现是如果运行的程序只剩下守护线程的话,程序将终止运行,直接结束。所以守护线程是作为辅助线程存在的。应用程序里的线程,一般都是用户自定义线程,用户也可以在应用程序代码自定义守护线程,只需要调用Thread类的t.setDaemon(true);设置一下即可。

· 线程和进程的差别是什么?

· Java里面的Threadlocal是怎样实现的?

· ConcurrentHashMap的实现原理是?

在ConcurrentHashMap中,它使用了多个锁代替HashTable中的单个锁,也就是锁分离技术(Lock Stripping)。 每个hash区间使用的ReentrantLock锁。
ConcurrentHashMap源码剖析

· sleep和wait区别

  1. 这两个方法来自不同的类分别是Thread和Object
  2. 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用.
    synchronized(x){
    x.notify() //或者wait()
    }
    
  4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

· notify和notifyAll区别

  1. 由其他线程notify或notifyAll了,并且当前线程被通知到了
  2. 经过和其他线程进行锁竞争,成功获取到锁了

· 什么是条件锁、读写锁、自旋锁、可重入锁?

  1. 条件锁
    在lock中提供了与之关联的条件,一个锁可能关联一个或多个条件,这些条件通过condition接口声明。目的是运行线程获取锁并且查看等待某一个条件是否满足,如果不满足则挂起直到某个线程唤醒它们。condition接口提供了挂起线程和唤起线程的机制;

  2. 读写锁
    Java中读写锁有个接口java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock,它们不是继承关系,但都是基于 AbstractQueuedSynchronizer来实现。ReentrantReadWriteLock的锁策略有两种,分为公平策略和非公平策略

    注意: 在同一线程中,持有读锁后,不能直接调用写锁的lock方法 ,否则会造成死锁。。

  3. 可重入(Reentrant)锁
    如果锁具备可重入性,则称作为可重入锁。像synchronized和 ReentrantLock都是可重入锁。假如某一时刻,线程A执行到了method1,此时线程 A获取了这个对象的锁,而由于method2也是synchronized方法,因为可重入,线程A不需要申请加锁即可执行。(可以理解锁的维度是线程,所以已拥有锁不需要再次申请加锁)。不可重入锁(自旋锁):不可以再次进入方法A,也就是说获得锁进入方法A是此线程在释放锁钱唯一的一次进入方法A。

  4. 可中断锁
    可中断锁:顾名思义,就是可以相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

· 线程池ThreadPoolExecutor的几个参数说明?

上一篇下一篇

猜你喜欢

热点阅读