java面试_java面试题 - CSDN
精华内容
参与话题
  • Java面试Offer直通车

    万人学习 2020-07-21 20:25:45
    Java面试宝典】 1、68讲视频课,500道大厂Java常见面试题+100个Java面试技巧与答题公式+10万字核心知识解析+授课老师1对1面试指导+无限次回放 2、这门课程基于胡书敏老师8年Java面试经验,调研近百家互联网公司...
  • 史上最全面Java面试汇总(面试题+答案)

    万次阅读 多人点赞 2020-10-30 11:33:29
    JAVA面试精选【Java基础第一部分】 JAVA面试精选【Java基础第二部分】 JAVA面试精选【Java基础第三部分】 JAVA面试精选【Java算法与编程一】 JAVA面试精选【Java算法与编程二】 Java高级工程师—面试(1) ...

    微信搜索:“二十同学” 公众号,欢迎关注一条不一样的成长之路

    JAVA面试精选【Java基础第一部分】

    JAVA面试精选【Java基础第二部分】

    JAVA面试精选【Java基础第三部分】

    JAVA面试精选【Java算法与编程一】

    JAVA面试精选【Java算法与编程二】

     

    Java高级工程师—面试(1)

    Java高级工程师—面试(2)

    Java高级工程师—面试(3)

    BAT/网易等面试心得

    阿里历年面试题

    Java中高级面试题

    数据库性能优化

     

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

    面向过程
    优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
    缺点:没有面向对象易维护、易复用、易扩展
    面向对象
    优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
    缺点:性能比面向过程低

    2.Java的四个基本特性(抽象、封装、继承,多态)

    抽象:就是把现实生活中的某一类东西提取出来,用程序代码表示,我们通常叫做类或者接口。抽象包括两个方面:一个是数据抽象,一个是过程抽象。数据抽象也就是对象的属性。过程抽象是对象的行为特征。
    封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行封装隐藏。封装分为属性的封装和方法的封装。
    继承:是对有着共同特性的多类事物,进行再抽象成一个类。这个类就是多类事物的父类。父类的意义在于抽取多类事物的共性。
    多态:允许不同类的对象对同一消息做出响应。方法的重载、类的覆盖正体现了多态。

    3.重载和重写的区别

    重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
    重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类;如果父类方法访问修饰符为private则子类中就不是重写。

    4.构造器Constructor是否可被override

    构造器不能被重写,不能用static修饰构造器,只能用public
    private protected这三个权限修饰符,且不能有返回语句。

    5.访问控制符public,protected,private,以及默认的区别

    private只有在本类中才能访问;
    public在任何地方都能访问;
    protected在同包内的类及包外的子类能访问;
    默认不写在同包内能访问。

    6是否可以继承String类

    String类是final类故不可以继承,一切由final修饰过的都不能继承。

    7.String和StringBuffer、StringBuilder的区别

    可变性
    String类中使用字符数组保存字符串,private
    final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[]
    value,这两种对象都是可变的。
    线程安全性
    String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
    性能
    每次对String 类型进行改变的时候,都会生成一个新的String 对象,然后将指针指向新的String 对象。StringBuffer每次都会对
    StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用
    StirngBuilder 相比使用
    StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

    8.hashCode和equals方法的关系

    equals相等,hashcode必相等;hashcode相等,equals可能不相等。

    9.抽象类和接口的区别

    语法层次
    抽象类和接口分别给出了不同的语法定义。
    设计层次
    抽象层次不同,抽象类是对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
    跨域不同
    抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is-a"
    关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的,仅仅是实现了接口定义的契约而已,"like-a"的关系。

    10.自动装箱与拆箱

    装箱:将基本类型用它们对应的引用类型包装起来;
    拆箱:将包装类型转换为基本数据类型;
    Java使用自动装箱和拆箱机制,节省了常用数值的内存开销和创建对象的开销,提高了效率,由编译器来完成,编译器会在编译期根据语法决定是否进行装箱和拆箱动作。

    11.什么是泛型、为什么要使用以及泛型擦除

    泛型,即“参数化类型”。
    创建集合时就指定集合元素的类型,该集合只能保存其指定类型的元素,避免使用强制类型转换。
    Java编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程即类型擦除。泛型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。
    类型擦除的主要过程如下:
    1).将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
    2).移除所有的类型参数。

    12.Java中的集合类及关系图

    List和Set继承自Collection接口。
    Set无序不允许元素重复。HashSet和TreeSet是两个主要的实现类。
    List有序且允许元素重复。ArrayList、LinkedList和Vector是三个主要的实现类。
    Map也属于集合系统,但和Collection接口没关系。Map是key对value的映射集合,其中key列就是一个集合。key不能重复,但是value可以重复。HashMap、TreeMap和Hashtable是三个主要的实现类。
    SortedSet和SortedMap接口对元素按指定规则排序,SortedMap是对key列进行排序。

    13.HashMap实现原理

    具体原理参考文章:
    http://zhangshixi.iteye.com/blog/672697
    http://www.admin10000.com/document/3322.html

    14.HashTable实现原理

    具体原理参考文章:
    http://www.cnblogs.com/skywang12345/p/3310887.html
    http://blog.csdn.net/chdjj/article/details/38581035

    15.HashMap和HashTable区别

    1).HashTable的方法前面都有synchronized来同步,是线程安全的;HashMap未经同步,是非线程安全的。
    2).HashTable不允许null值(key和value都不可以) ;HashMap允许null值(key和value都可以)。
    3).HashTable有一个contains(Object
    value)功能和containsValue(Object
    value)功能一样。
    4).HashTable使用Enumeration进行遍历;HashMap使用Iterator进行遍历。
    5).HashTable中hash数组默认大小是11,增加的方式是old*2+1;HashMap中hash数组的默认大小是16,而且一定是2的指数。
    6).哈希值的使用不同,HashTable直接使用对象的hashCode; HashMap重新计算hash值,而且用与代替求模。

    16.ArrayList和vector区别

    ArrayList和Vector都实现了List接口,都是通过数组实现的。
    Vector是线程安全的,而ArrayList是非线程安全的。
    List第一次创建的时候,会有一个初始大小,随着不断向List中增加元素,当List 认为容量不够的时候就会进行扩容。Vector缺省情况下自动增长原来一倍的数组长度,ArrayList增长原来的50%。

    17.ArrayList和LinkedList区别及使用场景

    区别
    ArrayList底层是用数组实现的,可以认为ArrayList是一个可改变大小的数组。随着越来越多的元素被添加到ArrayList中,其规模是动态增加的。
    LinkedList底层是通过双向链表实现的, LinkedList和ArrayList相比,增删的速度较快。但是查询和修改值的速度较慢。同时,LinkedList还实现了Queue接口,所以他还提供了offer(),
    peek(), poll()等方法。
    使用场景
    LinkedList更适合从中间插入或者删除(链表的特性)。
    ArrayList更适合检索和在末尾插入或删除(数组的特性)。

    18.Collection和Collections的区别

    java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
    java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

    19.Concurrenthashmap实现原理

    具体原理参考文章:
    http://www.cnblogs.com/ITtangtang/p/3948786.html
    http://ifeve.com/concurrenthashmap/

    20.Error、Exception区别

    Error类和Exception类的父类都是throwable类,他们的区别是:
    Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
    Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

    21.Unchecked

    Exception和Checked Exception,各列举几个#
    Unchecked Exception:
    a. 指的是程序的瑕疵或逻辑错误,并且在运行时无法恢复。
    b. 包括Error与RuntimeException及其子类,如:OutOfMemoryError,
    UndeclaredThrowableException, IllegalArgumentException,
    IllegalMonitorStateException, NullPointerException, IllegalStateException,
    IndexOutOfBoundsException等。
    c. 语法上不需要声明抛出异常。

    Checked Exception:
    a. 代表程序不能直接控制的无效外界情况(如用户输入,数据库问题,网络异常,文件丢失等)
    b. 除了Error和RuntimeException及其子类之外,如:ClassNotFoundException,
    NamingException, ServletException, SQLException, IOException等。
    c. 需要try catch处理或throws声明抛出异常。

    22.Java中如何实现代理机制(JDK、CGLIB)

    JDK动态代理:代理类和目标类实现了共同的接口,用到InvocationHandler接口。
    CGLIB动态代理:代理类是目标类的子类,用到MethodInterceptor接口。

    23.多线程的实现方式

    继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。

    24.线程的状态转换

    25.如何停止一个线程

    参考文章:
    http://www.cnblogs.com/greta/p/5624839.html

    26.什么是线程安全

    线程安全就是多线程访问同一代码,不会产生不确定的结果。

    27.如何保证线程安全

    对非安全的代码进行加锁控制;
    使用线程安全的类;
    多线程并发情况下,线程共享的变量改为方法级的局部变量。

    28.synchronized如何使用

    synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
    1). 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
    2). 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
    3). 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
    4). 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

    29.synchronized和Lock的区别

    主要相同点:Lock能完成synchronized所实现的所有功能
    主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。Lock的锁定是通过代码实现的,而synchronized是在JVM层面上实现的,synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。Lock锁的范围有局限性,块范围,而synchronized可以锁住块、对象、类。

    30.多线程如何进行信息交互

    void notify() 唤醒在此对象监视器上等待的单个线程。
    void notifyAll() 唤醒在此对象监视器上等待的所有线程。
    void wait() 导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法。
    void wait(long timeout) 导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法,或者超过指定的时间量。
    void wait(long timeout, int nanos) 导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。

    31.sleep和wait的区别(考察的方向是是否会释放锁)

    sleep()方法是Thread类中方法,而wait()方法是Object类中的方法。
    sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态,在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备。

    32.多线程与死锁

    死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
    产生死锁的原因:
    一.因为系统资源不足。
    二.进程运行推进的顺序不合适。
    三.资源分配不当。

    33.如何才能产生死锁

    产生死锁的四个必要条件:
    一.互斥条件:所谓互斥就是进程在某一时间内独占资源。
    二.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    三.不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
    四.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

    34.死锁的预防

    打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
    一.打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
    二.打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。
    三.打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。
    四.打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。

    35.什么叫守护线程,用什么方法实现守护线程

    守护线程是为其他线程的运行提供服务的线程。
    setDaemon(boolean on)方法可以方便的设置线程的Daemon模式,true为守护模式,false为用户模式。

    36.Java线程池技术及原理

    参考文章:
    http://www.cnblogs.com/dolphin0520/p/3932921.html

    37.java并发包concurrent及常用的类

    这个内容有点多,参考文章:
    并发包诸类概览:http://www.raychase.net/1912
    线程池:http://www.cnblogs.com/dolphin0520/p/3932921.html
    锁:http://www.cnblogs.com/dolphin0520/p/3923167.html
    集合:http://www.cnblogs.com/huangfox/archive/2012/08/16/2642666.html

    38.volatile关键字

    用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。
    Java语言中的volatile变量可以被看作是一种“程度较轻的
    synchronized”;与
    synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是synchronized的一部分。锁提供了两种主要特性:互斥(mutual
    exclusion)和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的,如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。Volatile变量具有synchronized的可见性特性,但是不具备原子特性。这就是说线程能够自动发现volatile
    变量的最新值。
    要使volatile变量提供理想的线程安全,必须同时满足下面两个条件:对变量的写操作不依赖于当前值;该变量没有包含在具有其他变量的不变式中。
    第一个条件的限制使volatile变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而volatile不能提供必须的原子特性。实现正确的操作需要使x 的值在操作期间保持不变,而volatile
    变量无法实现这点。
    每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
    read and load 从主存复制变量到当前工作内存
    use and assign 执行代码,改变共享变量值
    store and write 用工作内存数据刷新主存相关内容
    其中use and
    assign 可以多次出现,但是这一些操作并不是原子性,也就是在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。

    39.Java中的NIO,BIO,AIO分别是什么

    BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
    NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
    AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理.AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

    40.IO和NIO区别

    一.IO是面向流的,NIO是面向缓冲区的。
    二.IO的各种流是阻塞的,NIO是非阻塞模式。
    三.Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

    41.序列化与反序列化

    把对象转换为字节序列的过程称为对象的序列化。
    把字节序列恢复为对象的过程称为对象的反序列化。
    对象的序列化主要有两种用途:
    一.把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
    二.在网络上传送对象的字节序列。
    当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

    42.常见的序列化协议有哪些

    Protobuf, Thrift, Hessian, Kryo

    43.内存溢出和内存泄漏的区别

    内存溢出是指程序在申请内存时,没有足够的内存空间供其使用,出现out of
    memory。
    内存泄漏是指分配出去的内存不再使用,但是无法回收。

    44.Java内存模型及各个区域的OOM,如何重现OOM

    这部分内容很重要,详细阅读《深入理解Java虚拟机》,也可以详细阅读这篇文章http://hllvm.group.iteye.com/group/wiki/2857-JVM

    45.出现OOM如何解决

    一. 可通过命令定期抓取heap dump或者启动参数OOM时自动抓取heap dump文件。
    二. 通过对比多个heap dump,以及heap dump的内容,分析代码找出内存占用最多的地方。
    三. 分析占用的内存对象,是否是因为错误导致的内存未及时释放,或者数据过多导致的内存溢出。

    46.用什么工具可以查出内存泄漏

    一. Memory
    Analyzer-是一款开源的JAVA内存分析软件,查找内存泄漏,能容易找到大块内存并验证谁在一直占用它,它是基于Eclipse
    RCP(Rich Client Platform),可以下载RCP的独立版本或者Eclipse的插件。
    二. JProbe-分析Java的内存泄漏。
    三.JProfiler-一个全功能的Java剖析工具,专用于分析J2SE和J2EE应用程序。它把CPU、执行绪和内存的剖析组合在一个强大的应用中,GUI可以找到效能瓶颈、抓出内存泄漏、并解决执行绪的问题。
    四. JRockit-用来诊断Java内存泄漏并指出根本原因,专门针对Intel平台并得到优化,能在Intel硬件上获得最高的性能。
    五. YourKit-.NET & Java Profiling业界领先的Java和.NET程序性能分析工具。
    六.AutomatedQA -AutomatedQA的获奖产品performance profiling和memory debugging工具集的下一代替换产品,支持Microsoft,Borland, Intel, Compaq 和 GNU编译器。可以为.NET和Windows程序生成全面细致的报告,从而帮助您轻松隔离并排除代码中含有的性能问题和内存/资源泄露问题。支持.Net 1.0,1.1,2.0,3.0和Windows 32/64位应用程序。
    七.Compuware DevPartner Java Edition-包含Java内存检测,代码覆盖率测试,代码性能测试,线程死锁,分布式应用等几大功能模块

    47.Java内存管理及回收算法

    阅读这篇文章:http://www.cnblogs.com/hnrainll/archive/2013/11/06/3410042.html

    48.Java类加载器及如何加载类(双亲委派)

    阅读文章:
    https://www.ibm.com/developerworks/cn/java/j-lo-classloader/(推荐)
    http://blog.csdn.net/zhoudaxia/article/details/35824249

    49.xml解析方式

    一.DOM(JAXP
    Crimson解析器)
    二.SAX
    三.JDOM
    四.DOM4J
    区别:
    一.DOM4J性能最好,连Sun的JAXM也在用DOM4J。目前许多开源项目中大量采用DOM4J,例如大名鼎鼎的hibernate也用DOM4J来读取XML配置文件。如果不考虑可移植性,那就采用DOM4J.
    二.JDOM和DOM在性能测试时表现不佳,在测试10M
    文档时内存溢出。在小文档情况下还值得考虑使用DOM和JDOM。虽然JDOM的开发者已经说明他们期望在正式发行版前专注性能问题,但是从性能观点来看,它确实没有值得推荐之处。另外,DOM仍是一个非常好的选择。DOM实现广泛应用于多种编程语言。它还是许多其它与XML相关的标准的基础,因为它正式获得W3C
    推荐(与基于非标准的Java模型相对),所以在某些类型的项目中可能也需要它(如在JavaScript中使用DOM)。
    三.SAX表现较好,这要依赖于它特定的解析方式-事件驱动。一个SAX检测即将到来的XML流,但并没有载入到内存(当然当XML流被读入时,会有部分文档暂时隐藏在内存中)。

    50.Statement和PreparedStatement之间的区别

    一.PreparedStatement是预编译的,对于批量处理可以大大提高效率. 也叫JDBC存储过程
    二.使用
    Statement 对象。在对数据库只执行一次性存取的时侯,用
    Statement 对象进行处理。PreparedStatement
    对象的开销比Statement大,对于一次性操作并不会带来额外的好处。
    三.statement每次执行sql语句,相关数据库都要执行sql语句的编译,preparedstatement是预编译得,
    preparedstatement支持批处理
    四.
    代码片段1:
    String updateString = "UPDATE COFFEES SET SALES = 75 " + "WHERE
    COF_NAME LIKE ′Colombian′";
    stmt.executeUpdate(updateString);
    代码片段2:
    PreparedStatement updateSales = con.prepareStatement("UPDATE COFFEES SET
    SALES = ? WHERE COF_NAME LIKE ? ");
    updateSales.setInt(1, 75);
    updateSales.setString(2, "Colombian");
    updateSales.executeUpdate();
    片断2和片断1的区别在于,后者使用了PreparedStatement对象,而前者是普通的Statement对象。PreparedStatement对象不仅包含了SQL语句,而且大多数情况下这个语句已经被预编译过,因而当其执行时,只需DBMS运行SQL语句,而不必先编译。当你需要执行Statement对象多次的时候,PreparedStatement对象将会大大降低运行时间,当然也加快了访问数据库的速度。
    这种转换也给你带来很大的便利,不必重复SQL语句的句法,而只需更改其中变量的值,便可重新执行SQL语句。选择PreparedStatement对象与否,在于相同句法的SQL语句是否执行了多次,而且两次之间的差别仅仅是变量的不同。如果仅仅执行了一次的话,它应该和普通的对象毫无差异,体现不出它预编译的优越性。
    五.执行许多SQL语句的JDBC程序产生大量的Statement和PreparedStatement对象。通常认为PreparedStatement对象比Statement对象更有效,特别是如果带有不同参数的同一SQL语句被多次执行的时候。PreparedStatement对象允许数据库预编译SQL语句,这样在随后的运行中可以节省时间并增加代码的可读性。
    然而,在Oracle环境中,开发人员实际上有更大的灵活性。当使用Statement或PreparedStatement对象时,Oracle数据库会缓存SQL语句以便以后使用。在一些情况下,由于驱动器自身需要额外的处理和在Java应用程序和Oracle服务器间增加的网络活动,执行PreparedStatement对象实际上会花更长的时间。
    然而,除了缓冲的问题之外,至少还有一个更好的原因使我们在企业应用程序中更喜欢使用PreparedStatement对象,那就是安全性。传递给PreparedStatement对象的参数可以被强制进行类型转换,使开发人员可以确保在插入或查询数据时与底层的数据库格式匹配。
    当处理公共Web站点上的用户传来的数据的时候,安全性的问题就变得极为重要。传递给PreparedStatement的字符串参数会自动被驱动器忽略。最简单的情况下,这就意味着当你的程序试着将字符串“D'Angelo”插入到VARCHAR2中时,该语句将不会识别第一个“,”,从而导致悲惨的失败。几乎很少有必要创建你自己的字符串忽略代码。
    在Web环境中,有恶意的用户会利用那些设计不完善的、不能正确处理字符串的应用程序。特别是在公共Web站点上,在没有首先通过PreparedStatement对象处理的情况下,所有的用户输入都不应该传递给SQL语句。此外,在用户有机会修改SQL语句的地方,如HTML的隐藏区域或一个查询字符串上,SQL语句都不应该被显示出来。

    51.servlet生命周期及各个方法

    参考文章http://www.cnblogs.com/xuekyo/archive/2013/02/24/2924072.html

    52.servlet中如何自定义filter

    参考文章http://www.cnblogs.com/javawebsoa/archive/2013/07/31/3228858.html

    53.JSP原理

    参考文章http://blog.csdn.net/hanxuemin12345/article/details/23831645

    54.JSP和Servlet的区别

    (1)JSP经编译后就变成了“类servlet”。
    (2)JSP由HTML代码和JSP标签构成,更擅长页面显示;Servlet更擅长流程控制。
    (3)JSP中嵌入JAVA代码,而Servlet中嵌入HTML代码。

    55.JSP的动态include和静态include

    (1)动态include用jsp:include动作实现,如<jsp:include page="abc.jsp" flush="true" />,它总是会检查所含文件中的变化,适合用于包含动态页面,并且可以带参数。会先解析所要包含的页面,解析后和主页面合并一起显示,即先编译后包含。
    (2)静态include用include伪码实现,不会检查所含文件的变化,适用于包含静态页面,如<%@
    include file="qq.htm" %>,不会提前解析所要包含的页面,先把要显示的页面包含进来,然后统一编译,即先包含后编译。

    56.Struts中请求处理过程

    参考文章http://www.cnblogs.com/liuling/p/2013-8-10-01.html

    57.MVC概念

    参考文章http://www.cnblogs.com/scwyh/articles/1436802.html

    58.Springmvc与Struts区别

    参考文章:
    http://blog.csdn.net/tch918/article/details/38305395
    http://blog.csdn.net/chenleixing/article/details/44570681

    59.Hibernate/Ibatis两者的区别

    参考文章http://blog.csdn.net/firejuly/article/details/8190229

    60.Hibernate一级和二级缓存

    参考文章http://blog.csdn.net/windrui/article/details/23165845

    61.简述Hibernate常见优化策略

    参考文章http://blog.csdn.net/shimiso/article/details/8819114

    62.Springbean的加载过程(推荐看Spring的源码)

     

    63.Springbean的实例化(推荐看Spring的源码)

     

    64.Spring如何实现AOP和IOC(推荐看Spring的源码)

    参考文章http://www.360doc.com/content/15/0116/21/12385684_441408260.shtml

    65.Springbean注入方式

    参考文章http://blessht.iteye.com/blog/1162131

    66.Spring的事务管理

    这个主题的参考文章没找到特别好的

    67.Spring事务的传播特性

    参考文章http://blog.csdn.net/lfsf802/article/details/9417095

    68.springmvc原理

    参考文章http://blog.sina.com.cn/s/blog_7ef0a3fb0101po57.html

    69.springmvc用过哪些注解

    参考文章http://aijuans.iteye.com/blog/2160141

    70.Restful有几种请求

    参考文章,http://www.infoq.com/cn/articles/designing-restful-http-apps-roth,该篇写的比较全。

    71.Restful好处

    (1)客户-服务器:客户-服务器约束背后的原则是分离关注点。通过分离用户接口和数据存储这两个关注点,改善了用户接口跨多个平台的可移植性;同时通过简化服务器组件,改善了系统的可伸缩性。
    (2)无状态:通信在本质上是无状态的,改善了可见性、可靠性、可伸缩性.
    (3)缓存:改善了网络效率减少一系列交互的平均延迟时间,来提高效率、可伸缩性和用户可觉察的性能。
    (4)统一接口:REST架构风格区别于其他基于网络的架构风格的核心特征是,它强调组件之间要有一个统一的接口。

    72.Tomcat,Apache,JBoss的区别

    Apache:HTTP服务器(WEB服务器),类似IIS,可以用于建立虚拟站点,编译处理静态页面,可以支持SSL技术,支持多个虚拟主机等功能。
    Tomcat:Servlet容器,用于解析jsp,Servlet的Servlet容器,是高效,轻量级的容器。缺点是不支持EJB,只能用于java应用。
    Jboss:应用服务器,运行EJB的J2EE应用服务器,遵循J2EE规范,能够提供更多平台的支持和更多集成功能,如数据库连接,JCA等,其对Servlet的支持是通过集成其他Servlet容器来实现的,如tomcat和jetty。

    73.memcached和redis的区别

    (1)性能对比:由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。
    (2)内存使用效率对比:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。
    (3)Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。

    74.如何理解分布式锁

    参考文章:
    http://blog.csdn.net/zheng0518/article/details/51607063
    http://blog.csdn.net/nicewuranran/article/details/51730131。

    75.你知道的开源协议有哪些

    常见的开源协议有GPL、LGPL、BSD、Apache Licence
    vesion 2.0、MIT,

    76.json和xml区别

    XML:
    (1)应用广泛,可扩展性强,被广泛应用各种场合;
    (2)读取、解析没有JSON快;
    (3)可读性强,可描述复杂结构。
    JSON:
    (1)结构简单,都是键值对;
    (2)读取、解析速度快,很多语言支持;
    (3)传输数据量小,传输速率大大提高;
    (4)描述复杂结构能力较弱。

    77.设计模式

    参考文章:http://www.cnblogs.com/beijiguangyong/archive/2010/11/15/2302807.html#_Toc281750445。

    78.设计模式的六大原则

     

    79.用一个设计模式写一段代码或画出一个设计模式的UML

    参考文章http://www.cnblogs.com/beijiguangyong/archive/2010/11/15/2302807.html#_Toc281750445

    80.高内聚,低耦合方面的理解

     

    81.深度优先和广度优先算法

    推荐看书籍复习!可参考文章:

    http://blog.csdn.net/andyelvis/article/details/1728378
    http://driftcloudy.iteye.com/blog/782873

    82.排序算法及对应的时间复杂度和空间复杂度

    推荐看书籍复习!可参考文章:
    http://www.cnblogs.com/liuling/p/2013-7-24-01.html
    http://blog.csdn.net/cyuyanenen/article/details/51514443
    http://blog.csdn.net/whuslei/article/details/6442755

    83.排序算法编码实现

    参考http://www.cnblogs.com/liuling/p/2013-7-24-01.html

    84.查找算法

     

    85.B+树

    参考http://www.cnblogs.com/syxchina/archive/2011/03/02/2197251.html

    86.KMP算法

    推荐阅读数据复习!参考http://www.cnblogs.com/c-cloud/p/3224788.html

    87.hash算法及常用的hash算法

    参考http://www.360doc.com/content/13/0409/14/10384031_277138819.shtml

    88.如何判断一个单链表是否有环

    参考文章:
    http://www.jianshu.com/p/0e28d31600dd
    http://my.oschina.net/u/2391658/blog/693277?p={{totalPage}}

    89.队列、栈、链表、树、堆、图

    推荐阅读数据复习!

    90.linux常用命令

    参考https://blog.csdn.net/qq_18298439/article/details/81737116

    91.如何查看内存使用情况

    参考http://blog.csdn.net/windrui/article/details/40046413

    92.Linux下如何进行进程调度

    推荐阅读书籍复习,参考文章:
    http://www.cnblogs.com/zhaoyl/archive/2012/09/04/2671156.html
    http://blog.csdn.net/rainharder/article/details/7975387

    93.产生死锁的必要条件

    参考http://blog.sina.com.cn/s/blog_5e3604840100ddgq.html

    94.死锁预防

    参考http://blog.sina.com.cn/s/blog_5e3604840100ddgq.html

    95.数据库范式

    参考http://www.360doc.com/content/12/0712/20/5287961_223855037.shtml

    96.数据库事务隔离级别

    参考http://blog.csdn.net/fg2006/article/details/6937413

    97.数据库连接池的原理

    参考http://blog.csdn.net/shuaihj/article/details/14223015

    98.乐观锁和悲观锁

    参考http://www.open-open.com/lib/view/open1452046967245.html

    99.如何实现不同数据库的数据查询分页

    参考http://blog.csdn.net/yztezhl/article/details/20489387

    100.SQL注入的原理,如何预防

     

    101.数据库索引的实现(B+树介绍、和B树、R树区别)

    参考文章:
    http://blog.csdn.net/kennyrose/article/details/7532032
    http://www.xuebuyuan.com/2216918.html

    102.SQL性能优化

    参考文章:
    http://database.51cto.com/art/200904/118526.htm
    http://www.cnblogs.com/rootq/archive/2008/11/17/1334727.html

    103.数据库索引的优缺点以及什么时候数据库索引失效

    参考文章:
    http://www.cnblogs.com/mxmbk/articles/5226344.html
    http://www.cnblogs.com/simplefrog/archive/2012/07/15/2592527.html
    http://www.open-open.com/lib/view/open1418476492792.html
    http://blog.csdn.net/colin_liu2009/article/details/7301089
    http://www.cnblogs.com/hongfei/archive/2012/10/20/2732589.html

    104.Redis的数据类型

    参考http://blog.csdn.net/hechurui/article/details/49508735

    105.OSI七层模型以及TCP/IP四层模型

    参考文章:
    http://blog.csdn.net/sprintfwater/article/details/8751453
    http://www.cnblogs.com/commanderzhu/p/4821555.html
    http://blog.csdn.net/superjunjin/article/details/7841099

    106.HTTP和HTTPS区别

    参考:
    http://blog.csdn.net/mingli198611/article/details/8055261
    http://www.mahaixiang.cn/internet/1233.html

    107.HTTP报文内容

    参考文章:
    https://yq.aliyun.com/articles/44675
    http://www.cnblogs.com/klguang/p/4618526.html
    http://my.oschina.net/orgsky/blog/387759

    108.get提交和post提交的区别

    参考文章:
    http://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html
     

    109.get提交是否有字节限制,如果有是在哪限制的

     

    110.TCP的三次握手和四次挥手

    阅读http://www.jianshu.com/p/f7d1010fa603

    111.session和cookie的区别

    参考http://www.cnblogs.com/shiyangxt/archive/2008/10/07/1305506.html

    112.HTTP请求中Session实现原理

    参考http://blog.csdn.net/zhq426/article/details/2992488

    113.redirect与forward区别

    参考http://www.cnblogs.com/wxgblogs/p/5602849.html

    114.TCP和UDP区别

    参考http://www.cnblogs.com/bizhu/archive/2012/05/12/2497493.html

    115.DDos攻击及预防

    参考文章:
    http://blog.csdn.net/huwei2003/article/details/45476743
    http://www.leiphone.com/news/201509/9zGlIDvLhwguqOtg.htm

     

    Java基础

    1. HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化。
    2. HaspMap扩容是怎样扩容的,为什么都是2的N次幂的大小。
    3. HashMap,HashTable,ConcurrentHashMap的区别。
    4. 极高并发下HashTable和ConcurrentHashMap哪个性能更好,为什么,如何实现的。
    5. HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么。
    6. java中四种修饰符的限制范围。
    7. Object类中的方法。
    8. 接口和抽象类的区别,注意JDK8的接口可以有实现。
    9. 动态代理的两种方式,以及区别。
    10. Java序列化的方式。
    11. 传值和传引用的区别,Java是怎么样的,有没有传值引用。
    12. 一个ArrayList在循环过程中删除,会不会出问题,为什么。
    13. @transactional注解在什么情况下会失效,为什么。

    数据结构和算法

    1. B+树
    2. 快速排序,堆排序,插入排序(其实八大排序算法都应该了解
    3. 一致性Hash算法,一致性Hash算法的应用

    JVM

    1. JVM的内存结构。
    2. JVM方法栈的工作过程,方法栈和本地方法栈有什么区别。
    3. JVM的栈中引用如何和堆中的对象产生关联。
    4. 可以了解一下逃逸分析技术。
    5. GC的常见算法,CMS以及G1的垃圾回收过程,CMS的各个阶段哪两个是Stop the world的,CMS会不会产生碎片,G1的优势。
    6. 标记清除和标记整理算法的理解以及优缺点。
    7. eden survivor区的比例,为什么是这个比例,eden survivor的工作过程。
    8. JVM如何判断一个对象是否该被GC,可以视为root的都有哪几种类型。
    9. 强软弱虚引用的区别以及GC对他们执行怎样的操作。
    10. Java是否可以GC直接内存。
    11. Java类加载的过程。
    12. 双亲委派模型的过程以及优势。
    13. 常用的JVM调优参数。
    14. dump文件的分析。
    15. Java有没有主动触发GC的方式(没有)。

    多线程

    1. Java实现多线程有哪几种方式。
    2. Callable和Future的了解。
    3. 线程池的参数有哪些,在线程池创建一个线程的过程。
    4. volitile关键字的作用,原理。
    5. synchronized关键字的用法,优缺点。
    6. Lock接口有哪些实现类,使用场景是什么。
    7. 可重入锁的用处及实现原理,写时复制的过程,读写锁,分段锁(ConcurrentHashMap中的segment)。
    8. 悲观锁,乐观锁,优缺点,CAS有什么缺陷,该如何解决。
    9. ABC三个线程如何保证顺序执行。
    10. 线程的状态都有哪些。
    11. sleep和wait的区别。
    12. notify和notifyall的区别。
    13. ThreadLocal的了解,实现原理。

    数据库相关

    1. 常见的数据库优化手段
    2. 索引的优缺点,什么字段上建立索引
    3. 数据库连接池。
    4. durid的常用配置。

    计算机网络

    1. TCP,UDP区别。
    2. 三次握手,四次挥手,为什么要四次挥手。
    3. 长连接和短连接。
    4. 连接池适合长连接还是短连接。

    设计模式

    1. 观察者模式
    2. 代理模式
    3. 单例模式,有五种写法,可以参考文章单例模式的五种实现方式
    4. 可以考Spring中使用了哪些设计模式

    分布式相关

    1. 分布式事务的控制。
    2. 分布式锁如何设计。
    3. 分布式session如何设计。
    4. dubbo的组件有哪些,各有什么作用。
    5. zookeeper的负载均衡算法有哪些。
    6. dubbo是如何利用接口就可以通信的。

    缓存相关

    1. redis和memcached的区别。
    2. redis支持哪些数据结构。
    3. redis是单线程的么,所有的工作都是单线程么。
    4. redis如何存储一个String的。
    5. redis的部署方式,主从,集群。
    6. redis的哨兵模式,一个key值如何在redis集群中找到存储在哪里。
    7. redis持久化策略。

    框架相关

    1. SpringMVC的Controller是如何将参数和前端传来的数据一一对应的。
    2. Mybatis如何找到指定的Mapper的,如何完成查询的。
    3. Quartz是如何完成定时任务的。
    4. 自定义注解的实现。
    5. Spring使用了哪些设计模式。
    6. Spring的IOC有什么优势。
    7. Spring如何维护它拥有的bean。
    展开全文
  • 2020JAVA面试题附答案(持续更新版)

    万次阅读 多人点赞 2020-04-21 23:52:57
    JAVA基础 JAVA中的几种基本类型,各占用多少字节? 下图单位是bit,非字节 1B=8bit String能被继承吗?为什么? 不可以,因为String类有final修饰符,而final修饰的类是不能被继承的,实现细节不允许改变。平常我们...

    前言


    前言:
    少年易老学难成,一寸光阴不可轻。未觉池塘春草梦,阶前梧叶已秋声 。—朱熹《劝学诗》
    勤奋才是改变你命运的唯一捷径。
    整理不易,各位看官点赞再看更舒适,养成好习惯(●´∀`●)


    1.JAVA基础


    1.JAVA中的几种基本类型,各占用多少字节?
    这里写图片描述
    下图单位是bit,非字节 1B=8bit
    在这里插入图片描述
    在这里插入图片描述
    2.String能被继承吗?为什么?
    不可以,因为String类有final修饰符,而final修饰的类是不能被继承的,实现细节不允许改变。平常我们定义的String str=”abc”(直接赋一个字面量);其实和String str=new String(“abc”)(通过构造器构造)还是有差异的。
    在这里插入图片描述

    String str=“abc”和String str=new String(“abc”); 产生几个对象?
        1.前者1或0,后者2或1,先看字符串常量池,如果字符串常量池中没有,都在常量池中创建一个,如果有,前者直接引用,后者在堆内存中还需创建一个“abc”实例对象。
    	2.对于基础类型的变量和常量:变量和引用存储在栈中,常量存储在常量池中。
    	3.为了提升jvm(JAVA虚拟机)性能和减少内存开销,避免字符的重复创建 项目中还是不要使用new String去创建字符串,最好使用String直接赋值。
    

    参考链接
    3.String, Stringbuffer, StringBuilder 的区别。
    String 字符串常量(final修饰,不可被继承),String是常量,当创建之后即不能更改。(可以通过StringBuffer和StringBuilder创建String对象(常用的两个字符串操作类)。)
    ==StringBuffer 字符串变量(线程安全),==其也是final类别的,不允许被继承,其中的绝大多数方法都进行了同步处理,包括常用的Append方法也做了同步处理(synchronized修饰)。其自jdk1.0起就已经出现。其toString方法会进行对象缓存,以减少元素复制开销。

    public synchronized String toString() {
    	if (toStringCache == null) {
    		toStringCache = Arrays.copyOfRange(value, 0, count);
    	}
    	return new String(toStringCache, true);
    }
    

    ==StringBuilder 字符串变量(非线程安全)==其自jdk1.5起开始出现。与StringBuffer一样都继承和实现了同样的接口和类,方法除了没使用synch修饰以外基本一致,不同之处在于最后toString的时候,会直接返回一个新对象。

    public String toString() {
    	// Create a copy, don’t share the array
    	return new String(value, 0, count);
    }
    

    4.ArrayList 和 LinkedList 有什么区别。
    ArrayList和LinkedList都实现了List接口,有以下的不同点:
    1、ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。
    2、相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。
    3、LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
    5.讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当 new 的时候, 他们的执行顺序。
    此题考察的是类加载器实例化时进行的操作步骤(加载–>连接->初始化)。
    父类静态代变量、
    父类静态代码块、
    子类静态变量、
    子类静态代码块、
    父类非静态变量(父类实例成员变量)、
    父类构造函数、
    子类非静态变量(子类实例成员变量)、
    子类构造函数。
    测试demo
    参阅博客《深入理解类加载》
    6.用过哪些 Map 类,都有什么区别,HashMap 是线程安全的吗,并发下使用的 Map 是什么,他们内部原理分别是什么,比如存储方式, hashcode,扩容, 默认容量等。
    hashMap是线程不安全的,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,采用哈希表来存储的,
    参考链接
    JAVA8 的 ConcurrentHashMap 为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。
    参考链接
    7.有没有有顺序的 Map 实现类, 如果有, 他们是怎么保证有序的。
    TreeMap和LinkedHashMap是有序的(TreeMap默认升序,LinkedHashMap则记录了插入顺序)。
    参考链接
    8.抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么。
    1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
    2、抽象类要被子类继承,接口要被类实现。
    3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现
    4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
    5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
    6、抽象方法只能申明,不能实现。abstract void abc();不能写成abstract void abc(){}。
    7、抽象类里可以没有抽象方法
    8、如果一个类里有抽象方法,那么这个类只能是抽象类
    9、抽象方法要被实现,所以不能是静态的,也不能是私有的。
    10、接口可继承接口,并可多继承接口,但类只能单根继承。
    9.继承和聚合的区别在哪。
    继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;在Java中此类关系通过关键字extends明确标识,在设计时一般没有争议性;
    这里写图片描述
    聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与CPU、公司与员工的关系等;表现在代码层面,和关联关系是一致的,只能从语义级别来区分;
    这里写图片描述
    参考链接
    10.讲讲你理解的 nio和 bio 的区别是啥,谈谈 reactor 模型。
    IO(BIO)是面向流的,NIO是面向缓冲区的
    BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
    NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
    AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
    参考链接1
    参考链接2
    11.反射的原理,反射创建类实例的三种方式是什么
    参考链接1
    参考链接2
    12.反射中,Class.forName 和 ClassLoader 区别。
    参考链接
    13.描述动态代理的几种实现方式,分别说出相应的优缺点。
    Jdk cglib jdk底层是利用反射机制,需要基于接口方式,这是由于

    Proxy.newProxyInstance(target.getClass().getClassLoader(),
    target.getClass().getInterfaces(), this);
    

    Cglib则是基于asm框架,实现了无反射机制进行代理,利用空间来换取了时间,代理效率高于jdk
    参考链接
    动态代理与 cglib 实现的区别
    同上(基于invocationHandler和methodInterceptor)
    14.为什么 CGlib 方式可以对接口实现代理。
    同上
    15.final 的用途
    类、变量、方法
    final 修饰的类叫最终类,该类不能被继承。
    final 修饰的方法不能被重写。
    final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
    参考链接
    16.写出三种单例模式实现。
    懒汉式单例,饿汉式单例,双重检查等
    参考链接
    17.如何在父类中为子类自动完成所有的 hashcode 和 equals 实现?这么做有何优劣。
    同时复写hashcode和equals方法,优势可以添加自定义逻辑,且不必调用超类的实现。
    参考链接
    18.请结合 OO 设计理念,谈谈访问修饰符 public、private、protected、default 在应用设计中的作用。
    访问修饰符,主要标示修饰块的作用域,方便隔离防护

    类中的数据成员和成员函数据具有的访问权限包括:publicprivate、protect、default(包访问权限)
    作用域		当前类	同一package	子孙类	其他package 
    public   	√    	√         	√       √ 
    protected   √      	√           √       × 
    default     √       √           ×       × 
    private     √       ×           ×       × 
    
    public           所有类可见
    protected      	 本包和所有子类都可见(本包中的子类非子类均可访问,不同包中的子类可以访问,不是子类不能访问)
    default          本包可见(即默认的形式)(本包中的子类非子类均可访问,不同包中的类及子类均不能访问)
    priavte          本类可见
    

    public: Java语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。
    private: Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。
    protect: 介于public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
    default:即不加任何访问修饰符,通常称为"默认访问模式"。该模式下,只允许在同一个包中进行访问。
    19.深拷贝和浅拷贝区别。
    参考链接
    20.数组和链表数据结构描述,各自的时间复杂度
    参考链接
    21.error 和 exception 的区别,CheckedException,RuntimeException 的区别
    参考链接
    22.请列出 5 个运行时异常。
    同上
    23.在自己的代码中,如果创建一个 java.lang.String 对象,这个对象是否可以被类加载器加载?为什么
    类加载无须等到“首次使用该类”时加载,jvm允许预加载某些类。。。。
    参考链接
    24.说一说你对 java.lang.Object 对象中 hashCode 和 equals 方法的理解。在什么场景下需要重新实现这两个方法。
    参考上边试题(17)
    25.在 jdk1.5 中,引入了泛型,泛型的存在是用来解决什么问题。
    泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率
    参考链接
    26.这样的 a.hashcode() 有什么用,与 a.equals(b)有什么关系
    hashcode
    hashcode()方法提供了对象的hashCode值,是一个native方法,返回的默认值与System.identityHashCode(obj)一致。
    通常这个值是对象头部的一部分二进制位组成的数字,具有一定的标识对象的意义存在,但绝不定于地址。
    作用是:用一个数字来标识对象。比如在HashMap、HashSet等类似的集合类中,如果用某个对象本身作为Key,即要基于这个对象实现Hash的写入和查找,那么对象本身如何实现这个呢?就是基于hashcode这样一个数字来完成的,只有数字才能完成计算和对比操作。
    hashcode是否唯一
    hashcode只能说是标识对象,在hash算法中可以将对象相对离散开,这样就可以在查找数据的时候根据这个key快速缩小数据的范围,但hashcode不一定是唯一的,所以hash算法中定位到具体的链表后,需要循环链表,然后通过equals方法来对比Key是否是一样的。
    equals与hashcode的关系
    equals相等两个对象,则hashcode一定要相等。但是hashcode相等的两个对象不一定equals相等。
    参考链接
    27.有没有可能 2 个不相等的对象有相同的 hashcode。

    实例:

    String str1 = "通话";
    String str2 = "重地";
    System.out.println(String.format("str1:%d | str2:%d",  str1.hashCode(),str2.hashCode()));
    System.out.println(str1.equals(str2));
    
    执行结果:str1:1179395 | str2:1179395
    false
    

    28.Java 中的 HashSet 内部是如何工作的。
    底层是基于hashmap实现的
    参考链接
    什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决。
    参考链接
    29. JDK 和 JRE 有什么区别?
    JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。
    JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境。
    具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。
    30. == 和 equals 的区别是什么?
    == 解读:
    对于基本类型和引用类型 == 的作用效果是不同的,如下所示:
    基本类型:比较的是值是否相同;
    引用类型:比较的是引用是否相同;
    实例:

            String x = "string";
            String y = "string";
            String z = new String("string");
            System.out.println(x==y); // true,引用相同
            System.out.println(x==z); // false,==:string比较引用,开辟了新的堆内存空间,所以false
            System.out.println(x.equals(y)); // true,equals:string:比较值,相同
            System.out.println(x.equals(z)); // true,equals:string比较值,相同
    

    equals 解读:
    equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。看下面的代码就明白了。
    首先来看默认情况下 equals 比较一个(有相同值的对象),代码如下:

    public class Cat {
        private  String name;
    	public Cat(String name){
            this.name = name;
        }
    	public String getName() {
            return name;
        }
    	public void setName(String name) {
            this.name = name;
        }
    	public static void main(String[] args) {
    		Cat c1 = new Cat("cat1");//c1是Cat的实例化对象,c2同理
            Cat c2 = new Cat("cat2");
           	String s1 = new String("隔壁老王");
            String s2 = new String("隔壁老王");
    		System.out.println(c1.equals(c2));//false,equals在比较的类对象的时候比较的是引用
    		System.out.println(s1.equals(s2)); //true,而在比较string的时候,因为重写了equals方法,和基本数据类型一样,比较的是值,所以为true
    
    }
    

    总结 :== 对于基本类型来说是值比较(不难理解,八种基本数据类型是可以有确定值的),对于引用类型来说是比较的是引用(数组、类、接口没有确定值);而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
    31.java 中的 Math.round(-1.5) 等于多少?
    等于 -1,因为在数轴上取值时,中间值(0.5)向右取整,所以正 0.5 是往上取整,负 0.5 是直接舍弃。同理,Math.round(1.5) = 2
    32.写一个字符串反转函数。

    // StringBuffer reverse
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("abcdefg");
    System.out.println(stringBuffer.reverse()); // gfedcba
    // StringBuilder reverse
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("abcdefg");
    System.out.println(stringBuilder.reverse()); // gfedcba
    

    33.String 类的常用方法都有那些?
    indexOf():返回指定字符的索引。
    charAt():返回指定索引处的字符。
    replace():字符串替换。
    trim():去除字符串两端空白。
    split():分割字符串,返回一个分割后的字符串数组。
    getBytes():返回字符串的 byte 类型数组。
    length():返回字符串长度。
    toLowerCase():将字符串转成小写字母。
    toUpperCase():将字符串转成大写字符。
    substring():截取字符串。
    equals():字符串比较。
    34.抽象类必须要有抽象方法吗?
    不需要,抽象类不一定非要有抽象方法。
    示例代码:

    public abstract class noAbstractMethod{
            public static void main(String[] args) {
                sayHi();
            }
            public static void sayHi() {
                System.out.println("hi~");
            }
    }
            结果:hi~
    

    35.java 中 IO 流分为几种?
    按功能来分:输入流(input)、输出流(output)。

    按类型来分:字节流和字符流。

    字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。
    36.Files的常用方法都有哪些?
    Files.exists():检测文件路径是否存在。
    Files.createFile():创建文件。
    Files.createDirectory():创建文件夹。
    Files.delete():删除一个文件或目录。
    Files.copy():复制文件。
    Files.move():移动文件。
    Files.size():查看文件个数。
    Files.read():读取文件。
    Files.write():写入文件。
    37.List、Set、Map 之间的区别是什么?
    在这里插入图片描述
    38.如何实现数组和 List 之间的转换?
    List转换成为数组:调用ArrayList的toArray方法。
    数组转换成为List:调用Arrays的asList方法。
    39. ArrayList 和 Vector 的区别是什么?
    Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。
    ArrayList比Vector快,它是异步,不会过载。
    ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。
    40.Array 和 ArrayList 有何区别?
    Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
    Array是指定大小的,而ArrayList初始大小是固定的。
    Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。
    41.在 Queue 中 poll()和 remove()有什么区别?
    poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
    42. 哪些集合类是线程安全的?
    vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
    statck:堆栈类,先进后出。
    hashtable:就比hashmap多了个线程安全。
    enumeration:枚举,相当于迭代器。
    43.迭代器 Iterator 是什么?
    迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。
    44.Iterator 怎么使用?有什么特点?
    Java中的Iterator功能比较简单,并且只能单向移动:

    (1) 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,公共基类Collection提供iterator()方法。

    (2) 使用next()获得序列中的下一个元素。

    (3) 使用hasNext()检查序列中是否还有元素。

    (4) 使用remove()将迭代器新返回的元素删除。

    Iterator是Java迭代器最简单的实现,为List设计的ListIterator具有更多的功能,它可以从两个方向遍历List,也可以从List中插入和删除元素。
    45. Iterator 和 ListIterator 有什么区别?
    Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List,见名知意,Set并不能使用ListIterator
    Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
    ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
    46.synchronized 和 volatile 的区别是什么?
    volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
    volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
    volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
    volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
    volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
    47.给定一个文本,获取某字符串出现的次数

        public static void count(){
    
            File file = new File("C:\\Users\\Administrator\\Desktop\\行政区划配置表.txt");
            InputStream is = null;
            try {
                is = new FileInputStream(file);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            byte[] b = new byte[1024];
            int a = 0;
            try {
                a = is.read(b);
            } catch (IOException e) {
                e.printStackTrace();
            }
            String[] str = new String(b, 0, a).split("");
            int count = 0;
            for(int i = 0;i<str.length;i++){
                //设置查询统计的字符串为a
                if("a".equals(str[i])){
                    count++;
                }
            }
            System.out.println(count);
        }
    

    2.JVM 知识


    1.什么情况下会发生栈内存溢出。
    如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
    参考链接
    2.JVM 的内存结构,Eden 和 Survivor 比例。
    这里写图片描述
    eden 和 survior 是按8比1分配的
    参考链接
    3.jvm 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代,说说你知道的几种主要的jvm 参数。
    对象诞生即新生代->eden,在进行minor gc过程中,如果依旧存活,移动到from,变成Survivor,进行标记代数,如此检查一定次数后,晋升为老年代,
    参考链接1
    参考链接2
    参考链接3
    4.你知道哪几种垃圾收集器,各自的优缺点,重点讲下 cms,包括原理,流程,优缺点
    Serial、parNew、ParallelScavenge、SerialOld、ParallelOld、CMS、G1
    参考链接
    5.垃圾回收算法的实现原理。
    参考链接
    6.当出现了内存溢出,你怎么排错。
    首先分析是什么类型的内存溢出,对应的调整参数或者优化代码。
    参考链接
    7.JVM 内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等。
    内存屏障:为了保障执行顺序和可见性的一条cpu指令
    重排序:为了提高性能,编译器和处理器会对执行进行重拍
    happen-before:操作间执行的顺序关系。有些操作先发生。
    主内存:共享变量存储的区域即是主内存
    工作内存:每个线程copy的本地内存,存储了该线程以读/写共享变量的副本
    参考链接1
    参考链接2
    参考链接3
    8.简单说说你了解的类加载器。
    类加载器的分类(bootstrap,ext,app,curstom),类加载的流程(load-link-init)
    参考链接
    9.讲讲 JAVA 的反射机制。
    Java程序在运行状态可以动态的获取类的所有属性和方法,并实例化该类,调用方法的功能
    参考链接
    10.你们线上应用的 JVM 参数有哪些。
    -server
    -Xms6000M
    -Xmx6000M
    -Xmn500M
    -XX:PermSize=500M
    -XX:MaxPermSize=500M
    -XX:SurvivorRatio=65536
    -XX:MaxTenuringThreshold=0
    -Xnoclassgc
    -XX:+DisableExplicitGC
    -XX:+UseParNewGC
    -XX:+UseConcMarkSweepGC
    -XX:+UseCMSCompactAtFullCollection
    -XX:CMSFullGCsBeforeCompaction=0
    -XX:+CMSClassUnloadingEnabled
    -XX:-CMSParallelRemarkEnabled
    -XX:CMSInitiatingOccupancyFraction=90
    -XX:SoftRefLRUPolicyMSPerMB=0
    -XX:+PrintClassHistogram
    -XX:+PrintGCDetails
    -XX:+PrintGCTimeStamps
    -XX:+PrintHeapAtGC
    -Xloggc:log/gc.log
    11.g1 和 cms 区别,吞吐量优先和响应优先的垃圾收集器选择。
    Cms是以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现。比较占用cpu资源,切易造成碎片。
    G1是面向服务端的垃圾收集器,是jdk9默认的收集器,基于标记-整理算法实现。可利用多核、多cpu,保留分代,实现可预测停顿,可控。
    参考链接
    请解释如下 jvm 参数的含义:
    -server -Xms512m -Xmx512m -Xss1024K
    -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxTenuringThreshold=20
    XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly。
    Server模式启动
    最小堆内存512m
    最大512m
    每个线程栈空间1m
    永久代256m
    最大永久代512m
    最大转为老年代检查次数20
    Cms回收开启时机:内存占用80%
    命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期
    12.32位系统jvm堆内存如下哪一个设置是最大且有效的( B )
    A. -Xmx1000m
    B. -Xmx1500m
    C. -Xmx8G
    D. 无限
    JVM最大内存: 首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了。


    3.开源框架知识


    1.简单讲讲 tomcat 结构,以及其类加载器流程。
    Server- --多个service
    Container级别的:–>engine–》host–>context
    Listenter
    Connector
    Logging、Naming、Session、JMX等等
    这里写图片描述
    通过WebappClassLoader 加载class
    参考链接1
    参考链接2
    参考链接3
    参考链接4
    2.tomcat 如何调优,涉及哪些参数。
    硬件上选择,操作系统选择,版本选择,jdk选择,配置jvm参数,配置connector的线程数量,开启gzip压缩,trimSpaces,集群等
    a) 内存优化:主要是对Tomcat启动参数进行优化,我们可以在Tomcat启动脚本中修改它的最大内存数等等。

    b) 线程数优化:Tomcat的并发连接参数,主要在Tomcat配置文件中server.xml中配置,比如修改最小空闲连接线程数,用于提高系统处理性能等等。

    c) 优化缓存:打开压缩功能,修改参数,比如压缩的输出内容大小默认为2KB,可以适当的修改。
    参考链接
    3.讲讲 Spring 加载流程。
    通过listener入口,核心是在AbstractApplicationContext的refresh方法,在此处进行装载bean工厂,bean,创建bean实例,拦截器,后置处理器等。
    参考链接
    4.讲讲 Spring 事务的传播属性。
    七种传播属性。
    事务传播行为
    所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:
    ①TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
    ②TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
    ③TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    ④TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    ⑤TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
    ⑥TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
    ⑦TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
    参考链接
    5.Spring 如何管理事务的。
    编程式和声明式
    同上
    6.Spring 怎么配置事务(具体说出一些关键的 xml 元素)。
    配置事务的方法有两种:
    1)、基于XML的事务配置。

    <?xml version="1.0" encoding="UTF-8"?>
    <!-- from the file 'context.xml' -->  
    <beans xmlns="http://www.springframework.org/schema/beans"  
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
         xmlns:aop="http://www.springframework.org/schema/aop"  
         xmlns:tx="http://www.springframework.org/schema/tx"  
         xsi:schemaLocation="  
         http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
         http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd  
         http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">  
          
      <!-- 数据元信息 -->  
      <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">  
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>  
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>  
        <property name="username" value="root"/>  
        <property name="password" value="root"/>  
      </bean>  
      
      <!-- 管理事务的类,指定我们用谁来管理我们的事务-->  
      <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource"/>  
      </bean>   
      
      <!-- 首先我们要把服务对象声明成一个bean  例如HelloService -->  
      <bean id="helloService" class="com.yintong.service.HelloService"/>  
      
      <!-- 然后是声明一个事物建议tx:advice,spring为我们提供了事物的封装,这个就是封装在了<tx:advice/>-->
      <!-- <tx:advice/>有一个transaction-manager属性,我们可以用它来指定我们的事物由谁来管理。
          默认:事务传播设置是 REQUIRED,隔离级别是DEFAULT -->
      <tx:advice id="txAdvice" transaction-manager="txManager">  
          <!-- 配置这个事务建议的属性 -->  
          <tx:attributes>  
            <!-- 指定所有get开头的方法执行在只读事务上下文中 -->  
            <tx:method name="get*" read-only="true"/>  
            <!-- 其余方法执行在默认的读写上下文中 -->  
            <tx:method name="*"/>  
          </tx:attributes>  
      </tx:advice>  
        
      <!-- 我们定义一个切面,它匹配FooService接口定义的所有操作 -->  
      <aop:config>  
         <!-- <aop:pointcut/>元素定义AspectJ的切面表示法,这里是表示com.yintong.service.helloService包下的任意方法。 -->
         <aop:pointcut id="helloServiceOperation" expression="execution(* com.yintong.service.helloService.*(..))"/>  
         <!-- 然后我们用一个通知器:<aop:advisor/>把这个切面和tx:advice绑定在一起,表示当这个切面:fooServiceOperation执行时tx:advice定义的通知逻辑将被执行 -->
         <aop:advisor advice-ref="txAdvice" pointcut-ref="helloServiceOperation"/>  
      </aop:config>  
     
    </beans>  
    

    2)、基于注解方式的事务配置。
    @Transactional:直接在Java源代码中声明事务的做法让事务声明和将受其影响的代码距离更近了,而且一般来说不会有不恰当的耦合的风险,因为,使用事务性的代码几乎总是被部署在事务环境中。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"  
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
         xmlns:aop="http://www.springframework.org/schema/aop"  
         xmlns:tx="http://www.springframework.org/schema/tx"  
         xsi:schemaLocation="  
         http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
         http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd  
         http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">  
        
      <bean id="helloService" class="com.yintong.service.HelloService"/>  
      <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
         <property name="dataSource" ref="dataSource"/>  
      </bean>
      <!-- 配置注解事务 -->  
      <tx:annotation-driven transaction-manager="txManager"/>  
    </beans>
    

    参考链接
    7.说说你对 Spring 的理解,非单例注入的原理?它的生命周期?循环注入的原理, aop 的实现原理,说说 aop 中的几个术语,它们是怎么相互工作的。
    AOP与IOC的概念(即spring的核心)

    a) IOC:Spring是开源框架,使用框架可以使我们减少工作量,提高工作效率并且它是分层结构,即相对应的层处理对应的业务逻辑,减少代码的耦合度。而spring的核心是IOC控制反转和AOP面向切面编程。IOC控制反转主要强调的是程序之间的关系是由容器控制的,容器控制对象,控制了对外部资源的获取。而反转即为,在传统的编程中都是由我们创建对象获取依赖对象,而在IOC中是容器帮我们创建对象并注入依赖对象,正是容器帮我们查找和注入对象,对象是被获取,所以叫反转。

    b) AOP:面向切面编程,主要是管理系统层的业务,比如日志,权限,事物等。AOP是将封装好的对象剖开,找出其中对多个对象产生影响的公共行为,并将其封装为一个可重用的模块,这个模块被命名为切面(aspect),切面将那些与业务逻辑无关,却被业务模块共同调用的逻辑提取并封装起来,减少了系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

    核心组件:bean,context,core,单例注入是通过单例beanFactory进行创建,生命周期是在创建的时候通过接口实现开启,循环注入是通过后置处理器,aop其实就是通过反射进行动态代理,pointcut,advice等。
    Aop相关:
    参考链接
    8.Springmvc 中 DispatcherServlet 初始化过程。
    入口是web.xml中配置的ds,ds继承了HttpServletBean,FrameworkServlet,通过其中的init方法进行初始化装载bean和实例,initServletBean是实际完成上下文工作和bean初始化的方法。
    参考链接
    9.springMVC的执行流程
    springMVC是由dispatchservlet为核心的分层控制框架。首先客户端发出一个请求web服务器解析请求url并去匹配dispatchservlet的映射url,如果匹配上就将这个请求放入到dispatchservlet,dispatchservlet根据mapping映射配置去寻找相对应的handel,然后把处理权交给找到的handel,handel封装了处理业务逻辑的代码,当handel处理完后会返回一个逻辑视图modelandview给dispatchservlet,此时的modelandview是一个逻辑视图不是一个正式视图,所以dispatchservlet会通过viewresource视图资源去解析modelandview,然后将解析后的参数放到view中返回到客户端并展现。
    10.事物的理解

    a) 事物具有原子性,一致性,持久性,隔离性
    b) 原子性:是指在一个事物中,要么全部执行成功,要么全部失败回滚。
    c) 一致性:事物执行之前和执行之后都处于一致性状态
    d) 持久性:事物多数据的操作是永久性
    e) 隔离性:当一个事物正在对数据进行操作时,另一个事物不可以对数据进行操作,也就是多个并发事物之间相互隔离。
    

    4.操作系统


    1.Linux 系统下你关注过哪些内核参数,说说你知道的。
    这里写图片描述
    Tcp/ip io cpu memory
    net.ipv4.tcp_syncookies = 1
    #启用syncookies
    net.ipv4.tcp_max_syn_backlog = 8192
    #SYN队列长度
    net.ipv4.tcp_synack_retries=2
    #SYN ACK重试次数
    net.ipv4.tcp_fin_timeout = 30
    #主动关闭方FIN-WAIT-2超时时间
    net.ipv4.tcp_keepalive_time = 1200
    #TCP发送keepalive消息的频度
    net.ipv4.tcp_tw_reuse = 1
    #开启TIME-WAIT重用
    net.ipv4.tcp_tw_recycle = 1
    #开启TIME-WAIT快速回收
    net.ipv4.ip_local_port_range = 1024 65000
    #向外连接的端口范围
    net.ipv4.tcp_max_tw_buckets = 5000
    #最大TIME-WAIT数量,超过立即清除
    net.ipv4.tcp_syn_retries = 2
    #SYN重试次数
    echo “fs.file-max=65535” >> /etc/sysctl.conf
    sysctl -p
    参考链接
    2.Linux 下 IO 模型有几种,各自的含义是什么。
    阻塞式io,非阻塞io,io复用模型,信号驱动io模型,异步io模型。
    参考链接1
    参考链接2
    3.epoll 和 poll 有什么区别。
    select的本质是采用32个整数的32位,即32* 32= 1024来标识,fd值为1-1024。当fd的值超过1024限制时,就必须修改FD_SETSIZE的大小。这个时候就可以标识32* max值范围的fd。
    对于单进程多线程,每个线程处理多个fd的情况,select是不适合的。
    1.所有的线程均是从1-32*max进行扫描,每个线程处理的均是一段fd值,这样做有点浪费
    2.1024上限问题,一个处理多个用户的进程,fd值远远大1024
    所以这个时候应该采用poll,
    poll传递的是数组头指针和该数组的长度,只要数组的长度不是很长,性能还是很不错的,因为poll一次在内核中申请4K(一个页的大小来存放fd),尽量控制在4K以内,
    epoll还是poll的一种优化,返回后不需要对所有的fd进行遍历,在内核中维持了fd的列表。select和poll是将这个内核列表维持在用户态,然后传递到内核中。但是只有在2.6的内核才支持。
    epoll更适合于处理大量的fd ,且活跃fd不是很多的情况,毕竟fd较多还是一个串行的操作
    参考链接
    4.平时用到哪些 Linux 命令。
    Ls,find,tar,tail,cp,rm,vi,grep,ps,pkill等等
    参考链接
    5.用一行命令查看文件的最后五行。
    Tail -n 5 filename
    6.用一行命令输出正在运行的 java 进程。
    ps -ef|grep Java
    7.介绍下你理解的操作系统中线程切换过程。
    控制权的转换,根据优先级切换上下文(用户,寄存器,系统)
    参考链接
    8.进程和线程的区别。
    Linux 实现并没有区分这两个概念(进程和线程)
    进程:程序的一次执行
    线程:CPU的基本调度单位
    一个进程可以包含多个线程。
    参考链接

    5.多线程

    1.多线程的几种实现方式,什么是线程安全。
    实现runable接口,继承thread类。
    参考链接
    2.volatile 的原理,作用,能代替锁么。
    Volatile利用内存栅栏机制来保持变量的一致性。不能代替锁,其只具备数据可见性一致性,不具备原子性。
    参考链接
    3.画一个线程的生命周期状态图。
    新建,可运行,运行中,睡眠,阻塞,等待,死亡。
    这里写图片描述
    参考链接
    4.sleep 和 wait 的区别。
    Sleep是休眠线程,wait是等待,sleep是thread的静态方法,wait则是object的方法。
    Sleep依旧持有锁,并在指定时间自动唤醒。wait则释放锁。
    参考链接
    5.Lock 与 Synchronized 的区别。
    首先两者都保持了并发场景下的原子性和可见性,区别则是synchronized的释放锁机制是交由其自身控制,且互斥性在某些场景下不符合逻辑,无法进行干预,不可人为中断等。
    而lock常用的则有ReentrantLock和readwritelock两者,添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
    参考链接
    6.synchronized 的原理是什么,解释以下名词:重排序,自旋锁,偏向锁,轻量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁。
    Synchronized底层是通过监视器的enter和exit实现
    参考链接1
    参考链接2
    7.用过哪些原子类,他们的原理是什么。
    AtomicInteger; AtomicLong; AtomicReference; AtomicBoolean;基于CAS原语实现 ,比较并交换、加载链接/条件存储,最坏的情况下是旋转锁
    参考链接1
    参考链接2
    8.用过线程池吗,newCache 和 newFixed 有什么区别,他们的原理简单概括下,构造函数的各个参数的含义是什么,比如 coreSize,maxsize 等。
    newSingleThreadExecutor返回以个包含单线程的Executor,将多个任务交给此Exector时,这个线程处理完一个任务后接着处理下一个任务,若该线程出现异常,将会有一个新的线程来替代。
    newFixedThreadPool返回一个包含指定数目线程的线程池,如果任务数量多于线程数目,那么没有没有执行的任务必须等待,直到有任务完成为止。
    newCachedThreadPool根据用户的任务数创建相应的线程来处理,该线程池不会对线程数目加以限制,完全依赖于JVM能创建线程的数量,可能引起内存不足。
    底层是基于ThreadPoolExecutor实现,借助reentrantlock保证并发。
    coreSize核心线程数,maxsize最大线程数。
    参考链接
    9.线程池的关闭方式有几种,各自的区别是什么。
    Shutdown shutdownNow tryTerminate 清空工作队列,终止线程池中各个线程,销毁线程池
    参考链接
    10.假如有一个第三方接口,有很多个线程去调用获取数据,现在规定每秒钟最多有 10 个线程同时调用它,如何做到。
    ScheduledThreadPoolExecutor 设置定时,进行调度。

    public ScheduledThreadPoolExecutor(int corePoolSize,
    ThreadFactory threadFactory) {
    	super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
    	new DelayedWorkQueue(), threadFactory);
    }
    

    参考链接
    11.spring 的 controller 是单例还是多例,怎么保证并发的安全。
    单例
    通过单例工厂 DefaultSingletonBeanRegistry实现单例
    通过AsyncTaskExecutor保持安全
    12.用三个线程按顺序循环打印 abc 三个字母,比如 abcabcabc。

    public class ABC_Synch {
        public static class ThreadPrinter implements Runnable {
            private String name;
            private Object prev;
            private Object self;
            private ThreadPrinter(String name, Object prev, Object self) {
                this.name = name;
                this.prev = prev;
                this.self = self;
            }
            @Override
            public void run() {
                int count = 10;
                while (count > 0) {// 多线程并发,不能用if,必须使用whil循环
                    synchronized (prev) { // 先获取 prev 锁
                        synchronized (self) {// 再获取 self 锁
                            System.out.print(name);//打印
                            count--;
    
                            self.notifyAll();// 唤醒其他线程竞争self锁,注意此时self锁并未立即释放。
                        }
                        //此时执行完self的同步块,这时self锁才释放。
                        try {
                            prev.wait(); // 立即释放 prev锁,当前线程休眠,等待唤醒
                            /**
                             * JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。
                             */
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        public static void main(String[] args) throws Exception {
            Object a = new Object();
            Object b = new Object();
            Object c = new Object();
            ThreadPrinter pa = new ThreadPrinter("A", c, a);
            ThreadPrinter pb = new ThreadPrinter("B", a, b);
            ThreadPrinter pc = new ThreadPrinter("C", b, c);
    
            new Thread(pa).start();
            Thread.sleep(10);//保证初始ABC的启动顺序
            new Thread(pb).start();
            Thread.sleep(10);
            new Thread(pc).start();
            Thread.sleep(10);
        }
    }
    
    

    参考链接
    13.ThreadLocal 用过么,用途是什么,原理是什么,用的时候要注意什么。
    Threadlocal底层是通过threadlocalMap进行存储键值 每个ThreadLocal类创建一个Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。
    ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
    谁设置谁负责移除
    参考链接1
    参考链接2
    14.如果让你实现一个并发安全的链表,你会怎么做。
    Collections.synchronizedList() ConcurrentLinkedQueue
    参考链接
    15.有哪些无锁数据结构,他们实现的原理是什么。
    LockFree,CAS
    基于jdk提供的原子类原语实现,例如AtomicReference
    参考链接
    16.讲讲 java 同步机制的 wait 和 notify。
    首先这两个方法只能在同步代码块中调用,wait会释放掉对象锁,等待notify唤醒。
    参考链接
    17.多线程如果线程挂住了怎么办。
    根据具体情况(sleep,wait,join等),酌情选择notifyAll,notify进行线程唤醒。
    参考链接
    18.countdowlatch 和 cyclicbarrier 的内部原理和用法,以及相互之间的差别。
    CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它运行一个或者多个线程一直处于等待状态。
    CyclicBarrier要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
    CyclicBarrier初始化的时候,设置一个屏障数。线程调用await()方法的时候,这个线程就会被阻塞,当调用await()的线程数量到达屏障数的时候,主线程就会取消所有被阻塞线程的状态。
    前者是递减,不可循环,后者是递加,可循环用
    countdowlatch 基于abq cb基于ReentrantLock Condition
    参考链接1
    参考链接2
    19.使用 synchronized 修饰静态方法和非静态方法有什么区别。
    对象锁和类锁
    参考链接
    20.简述 ConcurrentLinkedQueue LinkedBlockingQueue 的用处和不同之处。
    LinkedBlockingQueue 是一个基于单向链表的、范围任意的(其实是有界的)、FIFO 阻塞队列。
    ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现,该算法在Michael & Scott算法上进行了一些修改, Michael & Scott算法的详细信息可以参见参考资料一。
    参考链接1
    参考链接2
    参考链接3
    ##导致线程死锁的原因?怎么解除线程死锁。
    死锁问题是多线程特有的问题,它可以被认为是线程间切换消耗系统性能的一种极端情况。在死锁时,线程间相互等待资源,而又不释放自身的资源,导致无穷无尽的等待,其结果是系统任务永远无法执行完成。死锁问题是在多线程开发中应该坚决避免和杜绝的问题。
    一般来说,要出现死锁问题需要满足以下条件:

    互斥条件:一个资源每次只能被一个线程使用。
    请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
    循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
    只要破坏死锁 4 个必要条件之一中的任何一个,死锁问题就能被解决。
    

    参考链接
    21.非常多个线程(可能是不同机器),相互之间需要等待协调,才能完成某种工作,问怎么设计这种协调方案。
    此问题的本质是保持顺序执行。可以使用executors
    #TCP 与 HTTP
    22.http1.0 和 http1.1 有什么区别。
    HTTP 1.0主要有以下几点变化:
    请求和相应可以由于多行首部字段构成
    响应对象前面添加了一个响应状态行
    响应对象不局限于超文本
    服务器与客户端之间的连接在每次请求之后都会关闭
    实现了Expires等传输内容的缓存控制
    内容编码Accept-Encoding、字符集Accept-Charset等协商内容的支持
    这时候开始有了请求及返回首部的概念,开始传输不限于文本(其他二进制内容)
    HTTP 1.1加入了很多重要的性能优化:持久连接、分块编码传输、字节范围请求、增强的缓存机制、传输编码及请求管道。
    参考链接
    23.TCP 三次握手和四次挥手的流程,为什么断开连接要 4 次,如果握手只有两次,会出现什么。

    第一次握手(SYN=1, seq=x):
    客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。
    发送完毕后,客户端进入 <code>SYN_SEND</code> 状态。
    
    第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):
    服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。
    发送完毕后,服务器端进入 <code>SYN_RCVD</code> 状态。
    
    第三次握手(ACK=1,ACKnum=y+1)
    客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1
    

    发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。
    第一次挥手(FIN=1,seq=x)
    假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。
    发送完毕后,客户端进入 FIN_WAIT_1 状态。
    第二次挥手(ACK=1,ACKnum=x+1)
    服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。
    发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。
    第三次挥手(FIN=1,seq=y)
    服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。
    发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。
    第四次挥手(ACK=1,ACKnum=y+1)
    客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。
    服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。
    客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。
    两次后会重传直到超时。如果多了会有大量半链接阻塞队列。
    参考链接1
    参考链接2
    24.TIME_WAIT 和 CLOSE_WAIT 的区别。
    TIME_WAIT状态就是用来重发可能丢失的ACK报文。
    TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关闭。
    25.说说你知道的几种 HTTP 响应码,比如 200, 302, 404。

    1xx:信息,请求收到,继续处理
    2xx:成功,行为被成功地接受、理解和采纳
    3xx:重定向,为了完成请求,必须进一步执行的动作
    4xx:客户端错误,请求包含语法错误或者请求无法实现
    5xx:服务器错误,服务器不能实现一种明显无效的请求
    200 ok 一切正常
    302 Moved Temporatily 文件临时移出
    404 not found
    

    参考链接
    26.当你用浏览器打开一个链接的时候,计算机做了哪些工作步骤。
    Dns解析–>端口分析–>tcp请求–>服务器处理请求–>服务器响应–>浏览器解析—>链接关闭
    27.TCP/IP 如何保证可靠性,说说 TCP 头的结构。
    使用序号,对收到的TCP报文段进行排序以及检测重复的数据;使用校验和来检测报文段的错误;使用确认和计时器来检测和纠正丢包或延时。//TCP头部,总长度20字节

    typedef struct _tcp_hdr
    	{
    		unsigned short src_port;    //源端口号
    		unsigned short dst_port;    //目的端口号
    		unsigned int seq_no;        //序列号
    		unsigned int ack_no;        //确认号
    		#if LITTLE_ENDIAN
    		unsigned char reserved_1:4; //保留6位中的4位首部长度
    		unsigned char thl:4;        //tcp头部长度
    		unsigned char flag:6;       //6位标志
    		unsigned char reseverd_2:2; //保留6位中的2位
    		#else
    		unsigned char thl:4;        //tcp头部长度
    		unsigned char reserved_1:4; //保留6位中的4位首部长度
    		unsigned char reseverd_2:2; //保留6位中的2位
    		unsigned char flag:6;       //6位标志
    		#endif
    		unsigned short wnd_size;    //16位窗口大小
    		unsigned short chk_sum;     //16位TCP检验和
    		unsigned short urgt_p;      //16为紧急指针
    	}
    tcp_hdr;
    

    参考链接
    28.如何避免浏览器缓存。
    无法被浏览器缓存的请求:
    HTTP信息头中包含Cache-Control:no-cache,pragma:no-cache,或Cache-Control:max-age=0等告诉浏览器不用缓存的请求
    需要根据Cookie,认证信息等决定输入内容的动态请求是不能被缓存的
    经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓存,参考《HTTPS的七个误解》)
    POST请求无法被缓存
    HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存
    参考链接
    29.简述 Http 请求 get 和 post 的区别以及数据包格式。
    这里写图片描述
    这里写图片描述
    参考链接1
    参考链接2
    30.简述 HTTP 请求的报文格式。
    参考上面
    31.HTTPS 的加密方式是什么,讲讲整个加密解密流程。
    加密方式是tls/ssl,底层是通过对称算法,非对称,hash算法实现
    客户端发起HTTPS请求 --》2. 服务端的配置 --》
    3. 传送证书 —》4. 客户端解析证书 5. 传送加密信息 6. 服务段解密信息 7. 传输加密后的信息 8. 客户端解密信息
    参考链接
    #架构设计与分布式
    32.常见的缓存策略有哪些,你们项目中用到了什么缓存系统,如何设计的。
    Cdn缓存,redis缓存,ehcache缓存等
    Cdn 图片资源 js等, redis一主一从 echcache缓存数据
    33.用 java 自己实现一个 LRU。

    final int cacheSize = 100;
    Map&lt;String, String&gt; map = new LinkedHashMap&lt;String, String&gt;((int) Math.ceil(cacheSize / 0.75f) + 1, 0.75f, true) {
    	@Override
    	protected boolean removeEldestEntry(Map.Entry&lt;String, String&gt; eldest) {
    		return size() &gt; cacheSize;
    	}
    };
    

    参考链接
    34.分布式集群下如何做到唯一序列号。
    Redis生成,mongodb的objectId,zk生成
    参考链接
    35.设计一个秒杀系统,30 分钟没付款就自动关闭交易。
    分流 – 限流–异步–公平性(只能参加一次)–用户体验(第几位,多少分钟,一抢完)
    容错处理
    Redis 队列 mysql
    30分钟关闭 可以借助redis的发布订阅机制 在失效时进行后续操作,其他mq也可以
    参考链接
    36.如何使用 redis 和 zookeeper 实现分布式锁?有什么区别优缺点,分别适用什么场景。
    首先分布式锁实现常见的有数据库锁(表记录),缓存锁,基于zk(临时有序节点可以实现的)的三种
    Redis适用于对性能要求特别高的场景。redis可以每秒执行10w次,内网延迟不超过1ms
    缺点是数据存放于内存,宕机后锁丢失。
    锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
    非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
    不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
    单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
    参考链接
    37.如果有人恶意创建非法连接,怎么解决。
    可以使用filter过滤处理
    38.分布式事务的原理,优缺点,如何使用分布式事务。
    Two Phase commit协议
    优点是可以管理多机事务,拥有无线扩展性 确定是易用性难,承担延时风险
    JTA,atomiks等
    参考链接
    39.什么是一致性 hash。
    一致性hash是一种分布式hash实现算法。满足平衡性 单调性 分散性 和负载。
    参考链接
    40.什么是 restful,讲讲你理解的 restful。
    REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。
    参考链接
    41.如何设计建立和保持 100w 的长连接。
    服务器内核调优(tcp,文件数),客户端调优,框架选择(netty)
    42.如何防止缓存雪崩。
    缓存雪崩可能是因为数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。
    解决思路:
    1,采用加锁计数,或者使用合理的队列数量来避免缓存失效时对数据库造成太大的压力。这种办法虽然能缓解数据库的压力,但是同时又降低了系统的吞吐量。
    2,分析用户行为,尽量让失效时间点均匀分布。避免缓存雪崩的出现。
    3,如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备,但是双缓存涉及到更新事务的问题,update可能读到脏数据,需要好好解决。
    参考链接
    43.解释什么是 MESI 协议(缓存一致性)。
    MESI是四种缓存段状态的首字母缩写,任何多核系统中的缓存段都处于这四种状态之一。我将以相反的顺序逐个讲解,因为这个顺序更合理:
    失效(Invalid)缓存段,要么已经不在缓存中,要么它的内容已经过时。为了达到缓存的目的,这种状态的段将会被忽略。一旦缓存段被标记为失效,那效果就等同于它从来没被加载到缓存中。
    共享(Shared)缓存段,它是和主内存内容保持一致的一份拷贝,在这种状态下的缓存段只能被读取,不能被写入。多组缓存可以同时拥有针对同一内存地址的共享缓存段,这就是名称的由来。
    独占(Exclusive)缓存段,和S状态一样,也是和主内存内容保持一致的一份拷贝。区别在于,如果一个处理器持有了某个E状态的缓存段,那其他处理器就不能同时持有它,所以叫“独占”。这意味着,如果其他处理器原本也持有同一缓存段,那么它会马上变成“失效”状态。
    已修改(Modified)缓存段,属于脏段,它们已经被所属的处理器修改了。如果一个段处于已修改状态,那么它在其他处理器缓存中的拷贝马上会变成失效状态,这个规律和E状态一样。此外,已修改缓存段如果被丢弃或标记为失效,那么先要把它的内容回写到内存中——这和回写模式下常规的脏段处理方式一样。
    44.说说你知道的几种 HASH 算法,简单的也可以。
    哈希(Hash)算法,即散列函数。 它是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。 同时,哈希函数可以将任意长度的输入经过变化以后得到固定长度的输出
    MD4 MD5 SHA
    参考链接
    45.什么是 paxos 算法。
    Paxos算法是莱斯利·兰伯特(Leslie Lamport,就是 LaTeX 中的"La",此人现在在微软研究院)于1990年提出的一种基于消息传递的一致性算法。
    参考链接
    ##什么是 zab 协议。
    ZAB 是 Zookeeper 原子广播协议的简称
    整个ZAB协议主要包括消息广播和崩溃恢复两个过程,进一步可以分为三个阶段,分别是:
    发现 Discovery
    同步 Synchronization
    广播 Broadcast
    组成ZAB协议的每一个分布式进程,都会循环执行这三个阶段,将这样一个循环称为一个主进程周期。
    参考链接
    ##一个在线文档系统,文档可以被编辑,如何防止多人同时对同一份文档进行编辑更新。
    点击编辑的时候,利用redis进行加锁setNX完了之后 expire 一下
    也可以用版本号进行控制
    46.线上系统突然变得异常缓慢,你如何查找问题。
    逐级排查(网络,磁盘,内存,cpu),数据库,日志,中间件等也可通过监控工具排查。
    47.说说你平时用到的设计模式。
    单例, 代理,模板,策略,命令
    a) 单例模式:单例模式核心只需要new一个实例对象的模式,比如数据库连接,在线人数等,一些网站上看到的在线人数统计就是通过单例模式实现的,把一个计时器存放在数据库或者内存中,当有人登陆的时候取出来加一再放回去,有人退出登陆的时候取出来减一再放回去,但是当有两个人同时登陆的时候,会同时取出计数器,同时加一,同时放回去,这样的话数据就会错误,所以需要一个全局变量的对象给全部人使用,只需要new出一个实例对象,这就是单例模式的应用,并且单例模式节省资源,因为它控制了实例对象的个数,并有利于gc回收。

    b) 策略模式:就是将几个类中公共的方法提取到一个新的类中,从而使扩展更容易,保证代码的可移植性,可维护性强。比如有个需求是写鸭子对象,鸭子有叫,飞,外形这三种方法,如果每个鸭子类都写这三个方法会出现代码的冗余,这时候我们可以把鸭子中的叫,飞,外形这三个方法提取出来,放到鸭父类中,让每个鸭子都继承这个鸭父类,重写这三个方法,这样封装的代码可移植性强,当用户提出新的需求比如鸭子会游泳,那么对于我们oo程序员来讲就非常简单了我们只需要在鸭父类中加一个游泳的方法,让会游泳的鸭子重写游泳方法就可以了。

    c) 工厂模式:简单的工厂模式主要是统一提供实例对象的引用,通过工厂模式接口获取实例对象的引用。比如一个登陆功能,后端有三个类,controller类,interface类,实现接口的实现类。当客户端发出一个请求,当请求传到controller类中时,controller获取接口的引用对象,而实现接口的实现类中封装好了登陆的业务逻辑代码。当你需要加一个注册需求的时候只需要在接口类中加一个注册方法,实现类中实现方法,controller获取接口的引用对象即可,不需要改动原来的代码,这种做法是的可拓展性强。
    参考链接
    48.Dubbo 的原理,数据怎么流转的,怎么实现集群,负载均衡,服务注册和发现。重试转发,快速失败的策略是怎样的。
    Dubbo[]是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
    ##Cluster 实现集群
    在集群负载均衡时,Dubbo提供了多种均衡策略,缺省为random随机调用。
    Random LoadBalance:随机,按权重比率设置随机概率。
    RoundRobin LoadBalance:轮循,按公约后的权重比率设置轮循比率。
    LeastActive LoadBalance:最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
    ConsistentHash LoadBalance:一致性Hash,相同参数的请求总是发到同一提供者。当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
    快速失败,只发起一次调用,失败立即报错。
    参考链接
    49.一次 RPC 请求的流程是什么。
    1)服务消费方(client)调用以本地调用方式调用服务;
    2)client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
    3)client stub找到服务地址,并将消息发送到服务端;
    4)server stub收到消息后进行解码;
    5)server stub根据解码结果调用本地的服务;
    6)本地服务执行并将结果返回给server stub;
    7)server stub将返回结果打包成消息并发送至消费方;
    8)client stub接收到消息,并进行解码;
    9)服务消费方得到最终结果。
    50.异步模式的用途和意义。
    异步模式使用与服务器多核,并发严重的场景
    可提高服务吞吐量大,不容易受到冲击,可以采用并发策略,提高响应时间
    缓存数据过期后的更新如何设计。
    失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
    命中:应用程序从cache中取数据,取到后返回。
    更新:先把数据存到数据库中,成功后,再让缓存失效。
    51.编程中自己都怎么考虑一些设计原则的,比如开闭原则,以及在工作中的应用。
    开闭原则(Open Close Principle)
    一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
    里氏代换原则(Liskov Substitution Principle)
    子类型必须能够替换掉它们的父类型。
    依赖倒转原则(Dependence Inversion Principle)
    高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。即针对接口编程,不要针对实现编程
    接口隔离原则(Interface Segregation Principle)
    建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少
    组合/聚合复用原则
    说要尽量的使用合成和聚合,而不是继承关系达到复用的目的
    迪米特法则(Law Of Demeter)
    迪米特法则其根本思想,是强调了类之间的松耦合,类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成影响,也就是说,信息的隐藏促进了软件的复用。
    单一职责原则(Single Responsibility Principle)
    一个类只负责一项职责,应该仅有一个引起它变化的原因
    参考链接
    52.设计一个社交网站中的“私信”功能,要求高并发、可扩展等等。 画一下架构图。
    MVC 模式,即常见的 MVC 框架。
    SSM SSH SSI等
    53.曾经参与设计的服务器架构。
    54.应用服务器怎么监控性能,各种方式的区别。
    55.如何设计一套高并发支付方案,架构如何设计。
    56.如何实现负载均衡,有哪些算法可以实现。
    57.Zookeeper 的用途,选举的原理是什么。
    58.Mybatis 的底层实现原理。
    59.请思考一个方案,设计一个可以控制缓存总体大小的自动适应的本地缓存。
    ##请思考一个方案,实现分布式环境下的 countDownLatch。
    60.后台系统怎么防止请求重复提交。
    可以通过token值进行防止重复提交,存放到redis中,在表单初始化的时候隐藏在表单中,添加的时候在移除。判断这个状态即可防止重复提交。
    如何看待缓存的使用(本地缓存,集中式缓存),简述本地缓存和集中式缓存和优缺点。本地缓存在并发使用时的注意事项。
    61.描述一个服务从发布到被消费的详细过程。
    ##讲讲你理解的服务治理。
    62.如何做到接口的幂等性。
    #算法
    63.10 亿个数字里里面找最小的 10 个。
    ##有 1 亿个数字,其中有 2 个是重复的,快速找到它,时间和空间要最优。
    64.2 亿个随机生成的无序整数,找出中间大小的值。
    65.给一个不知道长度的(可能很大)输入字符串,设计一种方案,将重复的字符排重。
    66.遍历二叉树。
    67.有 3n+1 个数字,其中 3n 个中是重复的,只有 1 个是不重复的,怎么找出来。
    ##常用的排序算法,快排,归并、冒泡。 快排的最优时间复杂度,最差复杂度。冒泡排序的优化方案。
    ##二分查找的时间复杂度,优势。
    ##一个已经构建好的 TreeSet,怎么完成倒排序。

    	//冒泡
        public static void mp(int a[]) {
    
            int swap = 0;
            for (int i = 0; i < a.length; i++) {
                for (int j = i; j < a.length; j++) {
                    if (a[j] > a[i]) {
                        swap = a[i];
                        a[i] = a[j];
                        a[j] = swap;
                    }
                }
            }
            System.out.println(Arrays.toString(a));
            }
    
        /**
         * 不使用递归的二分查找
         *title:commonBinarySearch
         *@param arr
         *@param key
         *@return 关键字位置
         */
        public static int commonBinarySearch(int[] arr,int key){
            int low = 0;
            int high = arr.length - 1;
            int middle = 0;//定义middle
    
            if(key < arr[low] || key > arr[high] || low > high){
                return -1;
            }
            while(low <= high){
                middle = (low + high) / 2;
                if(arr[middle] > key){
                    //比关键字大则关键字在左区域
                    high = middle - 1;
                }else if(arr[middle] < key){
                    //比关键字小则关键字在右区域
                    low = middle + 1;
                }else{
                    return middle;
                }
            }
            return -1;//最后仍然没有找到,则返回-1
        }
        
    	/**
    	 * 使用递归的二分查找
    	 *title:recursionBinarySearch
    	 *@param arr 有序数组
    	 *@param key 待查找关键字
    	 *@return 找到的位置
    	 */
    	public static int recursionBinarySearch(int[] arr,int key,int low,int high){
    		
    		if(key < arr[low] || key > arr[high] || low > high){
    			return -1;				
    		}
    		int middle = (low + high) / 2;//初始中间位置
    		if(arr[middle] > key){
    			//比关键字大则关键字在左区域
    			return recursionBinarySearch(arr, key, low, middle - 1);
    		}else if(arr[middle] < key){
    			//比关键字小则关键字在右区域
    			return recursionBinarySearch(arr, key, middle + 1, high);
    		}else {
    			return middle;
    		}	
    	}
    

    68.什么是 B+树,B-树,列出实际的使用场景。
    69.并行和并发有什么区别?
    并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。
    并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
    参考链接
    70.线程和进程的区别?
    简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
    71.守护线程是什么?
    守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。
    72. 创建线程有哪几种方式?
    ①. 继承Thread类创建线程类

    定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
    创建Thread子类的实例,即创建了线程对象。
    调用线程对象的start()方法来启动该线程。
    ②. 通过Runnable接口创建线程类

    定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    调用线程对象的start()方法来启动该线程。
    ③. 通过Callable和Future创建线程

    创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
    创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    使用FutureTask对象作为Thread对象的target创建并启动新线程。
    调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
    73. 说一下 runnable 和 callable 有什么区别?
    Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
    Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
    74.线程有哪些状态?
    线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

    创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
    就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
    运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
    阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
    死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪   
    75.sleep() 和 wait() 有什么区别?
    sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。

    wait():wait()是Object的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。
    76.notify()和 notifyAll()有什么区别?
    如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
    当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
    优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
    77.线程的 run()和 start()有什么区别?
    每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。

    start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

    run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
    78.创建线程池有哪几种方式?
    ①. newFixedThreadPool(int nThreads)

    创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。

    ②. newCachedThreadPool()

    创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。

    ③. newSingleThreadExecutor()

    这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。

    ④. newScheduledThreadPool(int corePoolSize)

    创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
    参考链接
    79.线程池都有哪些状态?
    线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。

    线程池各个状态切换框架图:
    在这里插入图片描述
    80.线程池中 submit()和 execute()方法有什么区别?
    接收的参数不一样
    submit有返回值,而execute没有
    submit方便Exception处理
    81. 在 java 程序中怎么保证多线程的运行安全?

    线程安全在三个方面体现:

    原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
    可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
    有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
    82.多线程锁的升级原理是什么?

    在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

    锁升级的图示过程:
    在这里插入图片描述
    83.什么是死锁?
    死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。
    84.ThreadLocal 是什么?有哪些使用场景?
    线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
    85.synchronized 和 Lock 有什么区别?
    首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
    synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
    synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
    用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
    synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
    Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
    86.对ajax的理解
    Ajax为异步请求,即局部刷新技术,在传统的页面中,用户需要点击按钮或者事件触发请求,到刷新页面,而异步技术为不需要点击即可触发事件,这样使得用户体验感增强,比如商城购物车的异步加载,当你点击商品时无需请求后台而直接动态修改参数。


    6.数据库知识


    1.数据库隔离级别有哪些,各自的含义是什么,MYSQL 默认的隔离级别是是什么。

    1.未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
    2.提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
    3.可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读
    4.串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
    
    MYSQL默认是RepeatedRead级别
    

    2.MYSQL 有哪些存储引擎,各自优缺点。

    1.MyISAM: 拥有较高的插入,查询速度,但不支持事务
    2.InnoDB :5.5版本后Mysql的默认数据库,事务型数据库的首选引擎,支持ACID事务,支持行级锁定
    3.BDB: 源自Berkeley DB,事务型数据库的另一种选择,支持COMMIT和ROLLBACK等其他事务特性
    4.Memory :所有数据置于内存的存储引擎,拥有极高的插入,更新和查询效率。但是会占用和数据量成正比的内存空间。并且其内容会在Mysql重新启动时丢失
    5.Merge :将一定数量的MyISAM表联合而成一个整体,在超大规模数据存储时很有用
    6.Archive :非常适合存储大量的独立的,作为历史记录的数据。因为它们不经常被读取。Archive拥有高效的插入速度,但其对查询的支持相对较差
    7.Federated: 将不同的Mysql服务器联合起来,逻辑上组成一个完整的数据库。非常适合分布式应用
    8.Cluster/NDB :高冗余的存储引擎,用多台数据机器联合提供服务以提高整体性能和安全性。适合数据量大,安全和性能要求高的应用
    9.CSV: 逻辑上由逗号分割数据的存储引擎。它会在数据库子目录里为每个数据表创建一个.CSV文件。这是一种普通文本文件,每个数据行占用一个文本行。CSV存储引擎不支持索引。
    10.BlackHole :黑洞引擎,写入的任何数据都会消失,一般用于记录binlog做复制的中继
    
    另外,Mysql的存储引擎接口定义良好。有兴趣的开发者通过阅读文档编写自己的存储引擎。
    

    参考链接
    3.高并发下,如何做到安全的修改同一行数据。
    使用悲观锁 悲观锁本质是当前只有一个线程执行操作,结束了唤醒其他线程进行处理。
    也可以缓存队列中锁定主键。
    4.乐观锁和悲观锁是什么,INNODB 的行级锁有哪 2 种,解释其含义。
    乐观锁是设定每次修改都不会冲突,只在提交的时候去检查,悲观锁设定每次修改都会冲突,持有排他锁。
    行级锁分为共享锁和排他锁两种 共享锁又称读锁 排他锁又称写锁
    参考链接
    5.SQL 优化的一般步骤是什么,怎么看执行计划,如何理解其中各个字段的含义。
    查看慢日志(show [session|gobal] status ),定位慢查询,查看慢查询执行计划 根据执行计划确认优化方案
    Explain sql
    select_type:表示select类型。常见的取值有SIMPLE(简单表,即不使用连接或者子查询)、PRIMARY(主查询,即外层的查询)、UNION(union中的第二个或者后面的查询语句)、SUBQUERY(子查询中的第一个SELECT)等。
    talbe:输出结果集的表。
    type:表的连接类型。性能由高到底:system(表中仅有一行)、const(表中最多有一个匹配行)、eq_ref、ref、ref_null、index_merge、unique_subquery、index_subquery、range、idnex等
    possible_keys:查询时,可能使用的索引
    key:实际使用的索引
    key_len:索引字段的长度
    rows:扫描行的数量
    Extra:执行情况的说明和描述
    Oracle优化

    1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
    
    2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
    
    select id from t where num is null
    可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
    select id from t where num=0
    3.应尽量避免在 where 子句中使用!=<>操作符,否则将引擎放弃使用索引而进行全表扫描。
    4.应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
    select id from t where num=10 or num=20
    可以这样查询:
    select id from t where num=10
    union all
    select id from t where num=20
    5.in 和 not in 也要慎用,否则会导致全表扫描,如:
    select id from t where num in(1,2,3)
    对于连续的数值,能用 between 就不要用 in 了:
    select id from t where num between 1 and 3
    6.下面的查询也将导致全表扫描:
    select id from t where name like ‘%abc%’
    若要提高效率,可以考虑全文检索。
    7.如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:
    select id from t where num=@num
    可以改为强制查询使用索引:
    select id from t with(index(索引名)) where num=@num
    8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
    select id from t where num/2=100
    应改为:
    select id from t where num=100*2
    9.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
    select id from t where substring(name,1,3)=‘abc’ // oracle总有的是substr函数。
    select id from t where datediff(day,createdate,2005-11-30)=0 //查过了确实没有datediff函数。
    应改为:
    select id from t where name like ‘abc%’
    select id from t where createdate>=2005-11-30’ and createdate<2005-12-1//
    oracle 中时间应该把char 转换成 date 如: createdate >= to_date(2005-11-30,‘yyyy-mm-dd’)
    10.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
    
    11.在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
    12.不要写一些没有意义的查询,如需要生成一个空表结构:
    select col1,col2 into #t from t where 1=0
    这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
    create table #t()
    13.很多时候用 exists 代替 in 是一个好的选择:
    select num from a where num in(select num from b)
    用下面的语句替换:
    select num from a where exists(select 1 from b where num=a.num)
    14.并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。
    15.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
    16.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。
    17.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
    18.尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
    19.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
    20.尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。
    21.避免频繁创建和删除临时表,以减少系统表资源的消耗。
    22.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。
    23.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。
    24.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
    25.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。
    26.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。
    27.与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。
    28.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。
    29.尽量避免大事务操作,提高系统并发能力。
    30.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
    

    参考链接1
    参考链接2
    6.数据库会死锁吗,举一个死锁的例子,mysql 怎么解决死锁。
    产生死锁的原因主要是:
    (1)系统资源不足。
    (2) 进程运行推进的顺序不合适。
    (3)资源分配不当等。
    如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
    产生死锁的四个必要条件:
    (1) 互斥条件:一个资源每次只能被一个进程使用。
    (2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    (3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
    (4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
    这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
    这里提供两个解决数据库死锁的方法:
    1)重启数据库(谁用谁知道)
    2)杀掉抢资源的进程:
    先查哪些进程在抢资源:SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
    杀掉它们:Kill trx_mysql_thread_id;
    7.MYsql 的索引原理,索引的类型有哪些,如何创建合理的索引,索引如何优化。
    索引是通过复杂的算法,提高数据查询性能的手段。从磁盘io到内存io的转变
    普通索引,主键,唯一,单列/多列索引建索引的几大原则
    1.最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
    2.=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式
    3.尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录
    4.索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);
    5.尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可
    参考链接1
    参考链接2
    ##聚集索引和非聚集索引的区别。
    “聚簇”就是索引和记录紧密在一起。
    非聚簇索引 索引文件和数据文件分开存放,索引文件的叶子页只保存了主键值,要定位记录还要去查找相应的数据块。
    8.数据库中 BTREE 和 B+tree 区别。
    B+是btree的变种,本质都是btree,btree+与B-Tree相比,B+Tree有以下不同点:
    每个节点的指针上限为2d而不是2d+1。
    内节点不存储data,只存储key;叶子节点不存储指针。
    参考链接
    Btree 怎么分裂的,什么时候分裂,为什么是平衡的。
    Key 超过1024才分裂B树为甚会分裂? 因为随着数据的增多,一个结点的key满了,为了保持B树的特性,就会产生分裂,就向红黑树和AVL树为了保持树的性质需要进行旋转一样!
    9.ACID 是什么。
    A,atomic,原子性,要么都提交,要么都失败,不能一部分成功,一部分失败。
    C,consistent,一致性,事物开始及结束后,数据的一致性约束没有被破坏
    I,isolation,隔离性,并发事物间相互不影响,互不干扰。
    D,durability,持久性,已经提交的事物对数据库所做的更新必须永久保存。即便发生崩溃,也不能被回滚或数据丢失。
    ##Mysql 怎么优化 table scan 的。
    避免在where子句中对字段进行is null判断
    应尽量避免在where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
    避免在where 子句中使用or 来连接条件
    in 和not in 也要慎用
    Like查询(非左开头)
    使用NUM=@num参数这种
    where 子句中对字段进行表达式操作num/2=XX
    在where子句中对字段进行函数操作
    10.如何写 sql 能够有效的使用到复合索引。
    由于复合索引的组合索引,类似多个木板拼接在一起,如果中间断了就无法用了,所以要能用到复合索引,首先开头(第一列)要用上,比如index(a,b) 这种,我们可以select table tname where a=XX 用到第一列索引 如果想用第二列 可以 and b=XX 或者and b like‘TTT%’
    11.mysql 中 in 和 exists 区别。
    mysql中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。一直大家都认为exists比in语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。
    如果查询的两个表大小相当,那么用in和exists差别不大。
    如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in:
    not in 和not exists如果查询语句使用了not in 那么内外表都进行全表扫描,没有用到索引;而not extsts 的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。
    1.EXISTS只返回TRUE或FALSE,不会返回UNKNOWN。
    2.IN当遇到包含NULL的情况,那么就会返回UNKNOWN。
    12.数据库自增主键可能的问题。
    在分库分表时可能会生成重复主键 利用自增比例达到唯一 自增1 2,3 等
    参考链接
    #消息队列
    ##用过哪些 MQ,和其他 mq 比较有什么优缺点,MQ 的连接是线程安全的吗,你们公司的MQ 服务架构怎样的。
    根据实际情况说明
    我们公司用activeMQ 因为业务比较简单 只有转码功能,而amq比较简单
    如果是分布式的建议用kafka
    参考链接
    13.MQ 系统的数据如何保证不丢失。
    基本都是对数据进行持久化,多盘存储
    14.rabbitmq 如何实现集群高可用。
    集群是保证服务可靠性的一种方式,同时可以通过水平扩展以提升消息吞吐能力。RabbitMQ是用分布式程序设计语言erlang开发的,所以天生就支持集群。接下来,将介绍RabbitMQ分布式消息处理方式、集群模式、节点类型,并动手搭建一个高可用集群环境,最后通过java程序来验证集群的高可用性。
    三种分布式消息处理方式
    RabbitMQ分布式的消息处理方式有以下三种:
    1、Clustering:不支持跨网段,各节点需运行同版本的Erlang和RabbitMQ, 应用于同网段局域网。
    2、Federation:允许单台服务器上的Exchange或Queue接收发布到另一台服务器上Exchange或Queue的消息, 应用于广域网,。
    3、Shovel:与Federation类似,但工作在更低层次。
    RabbitMQ对网络延迟很敏感,在LAN环境建议使用clustering方式;在WAN环境中,则使用Federation或Shovel。我们平时说的RabbitMQ集群,说的就是clustering方式,它是RabbitMQ内嵌的一种消息处理方式,而Federation或Shovel则是以plugin形式存在。
    参考链接1
    参考链接2
    #Redis,Memcached
    15.redis 的 list 结构相关的操作。
    LPUSH LPUSHX RPUSH RPUSHX LPOP RPOP BLPOP BRPOP LLEN LRANGE
    参考链接
    16.Redis 的数据结构都有哪些。
    字符串(strings):存储整数(比如计数器)和字符串(废话。。),有些公司也用来存储json/pb等序列化数据,并不推荐,浪费内存
    哈希表(hashes):存储配置,对象(比如用户、商品),优点是可以存取部分key,对于经常变化的或者部分key要求atom操作的适合
    列表(lists):可以用来存最新用户动态,时间轴,优点是有序,确定是元素可重复,不去重
    集合(sets):无序,唯一,对于要求严格唯一性的可以使用
    有序集合(sorted sets):集合的有序版,很好用,对于排名之类的复杂场景可以考虑
    参考链接
    ##Redis 的使用要注意什么,讲讲持久化方式,内存设置,集群的应用和优劣势,淘汰策略等。
    持久化方式:RDB时间点快照 AOF记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。
    内存设置 maxmemory used_memory
    虚拟内存: vm-enabled yes
    3.0采用Cluster方式,
    Redis集群相对单机在功能上存在一些限制, 需要开发人员提前了解,
    在使用时做好规避。 限制如下:
    1) key批量操作支持有限。 如mset、 mget, 目前只支持具有相同slot值的
    ke
    y执
    行批量操作。 对于映射为不同slot值的key由于执行mget、 mget等操作可
    能存在于多个节点上因此不被支持。
    2) key事务操作支持有限。 同理只支持多key在同一节点上的事务操
    作, 当多个key分布在不同的节点上时无法使用事务功能。
    3) key作为数据分区的最小粒度, 因此不能将一个大的键值对象如
    ha
    sh、 list等映射到不同的节点。
    4) 不支持多数据库空间。 单机下的Redis可以支持16个数据库, 集群模
    式下只能使用一个数据库空间, 即db0。
    5) 复制结构只支持一层, 从节点只能复制主节点, 不支持嵌套树状复
    制结构。
    Redis Cluster是Redis的分布式解决方案, 在3.0版本正式推出, 有效地解
    决了Redis分布式方面的需求。 当遇到单机内存、 并发、 流量等瓶颈时, 可
    以采用Cluster架构方案达到负载均衡的目的。 之前, Redis分布式方案一般
    有两种:
    ·客户端分区方案, 优点是分区逻辑可控, 缺点是需要自己处理数据路
    由、 高可用、 故障转移等问题。
    ·代理方案, 优点是简化客户端分布式逻辑和升级维护便利, 缺点是加
    重架构部署复杂度和性能损耗。
    现在官方为我们提供了专有的集群方案: Redis Cluster, 它非常优雅地
    解决了Redis集群方面的问题, 因此理解应用好Redis Cluster将极大地解放我
    们使用分布式Redis的工作量, 同时它也是学习分布式存储的绝佳案例。
    LRU(近期最少使用算法)TTL(超时算法) 去除ttl最大的键值
    参考链接1
    参考链接2
    参考链接3
    17.redis2 和 redis3 的区别,redis3 内部通讯机制。
    集群方式的区别,3采用Cluster,2采用客户端分区方案和代理方案
    通信过程说明:
    1) 集群中的每个节点都会单独开辟一个TCP通道, 用于节点之间彼此
    通信, 通信端口号在基础端口上加10000。
    2) 每个节点在固定周期内通过特定规则选择几个节点发送ping消息。
    3) 接收到ping消息的节点用pong消息作为响应。
    ##当前 redis 集群有哪些玩法,各自优缺点,场景。
    当缓存使用 持久化使用
    18.Memcache 的原理,哪些数据适合放在缓存中。
    基于libevent的事件处理
    内置内存存储方式SLab Allocation机制
    并不单一的数据删除机制
    基于客户端的分布式系统
    变化频繁,具有不稳定性的数据,不需要实时入库, (比如用户在线
    状态、在线人数…)
    门户网站的新闻等,觉得页面静态化仍不能满足要求,可以放入
    到memcache中.(配合jquey的ajax请求)
    ##redis 和 memcached 的内存管理的区别。
    Memcached默认使用Slab Allocation机制管理内存,其主要思想是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。
    Redis的内存管理主要通过源码中zmalloc.h和zmalloc.c两个文件来实现的。
    在Redis中,并不是所有的数据都一直存储在内存中的。这是和Memcached相比一个最大的区别。
    参考链接
    19.Redis 的并发竞争问题如何解决,了解 Redis 事务的 CAS 操作吗。
    Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。对此有2种解决方法:
    1.客户端角度,为保证每个客户端间正常有序与Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized。
    2.服务器角度,利用setnx实现锁。
    MULTI,EXEC,DISCARD,WATCH 四个命令是 Redis 事务的四个基础命令。其中:
    MULTI,告诉 Redis 服务器开启一个事务。注意,只是开启,而不是执行
    EXEC,告诉 Redis 开始执行事务
    DISCARD,告诉 Redis 取消事务
    WATCH,监视某一个键值对,它的作用是在事务执行之前如果监视的键值被修改,事务会被取消。
    可以利用watch实现cas乐观锁
    参考链接1
    参考链接2
    ##Redis 的选举算法和流程是怎样的
    Raft采用心跳机制触发Leader选举。系统启动后,全部节点初始化为Follower,term为0.节点如果收到了RequestVote或者AppendEntries,就会保持自己的Follower身份。如果一段时间内没收到AppendEntries消息直到选举超时,说明在该节点的超时时间内还没发现Leader,Follower就会转换成Candidate,自己开始竞选Leader。一旦转化为Candidate,该节点立即开始下面几件事情:
    1、增加自己的term。
    2、启动一个新的定时器。
    3、给自己投一票。
    4、向所有其他节点发送RequestVote,并等待其他节点的回复。
    如果在这过程中收到了其他节点发送的AppendEntries,就说明已经有Leader产生,自己就转换成Follower,选举结束。
    如果在计时器超时前,节点收到多数节点的同意投票,就转换成Leader。同时向所有其他节点发送AppendEntries,告知自己成为了Leader。
    每个节点在一个term内只能投一票,采取先到先得的策略,Candidate前面说到已经投给了自己,Follower会投给第一个收到RequestVote的节点。每个Follower有一个计时器,在计时器超时时仍然没有接受到来自Leader的心跳RPC, 则自己转换为Candidate, 开始请求投票,就是上面的的竞选Leader步骤。
    如果多个Candidate发起投票,每个Candidate都没拿到多数的投票(Split Vote),那么就会等到计时器超时后重新成为Candidate,重复前面竞选Leader步骤。
    Raft协议的定时器采取随机超时时间,这是选举Leader的关键。每个节点定时器的超时时间随机设置,随机选取配置时间的1倍到2倍之间。由于随机配置,所以各个Follower同时转成Candidate的时间一般不一样,在同一个term内,先转为Candidate的节点会先发起投票,从而获得多数票。多个节点同时转换为Candidate的可能性很小。即使几个Candidate同时发起投票,在该term内有几个节点获得一样高的票数,只是这个term无法选出Leader。由于各个节点定时器的超时时间随机生成,那么最先进入下一个term的节点,将更有机会成为Leader。连续多次发生在一个term内节点获得一样高票数在理论上几率很小,实际上可以认为完全不可能发生。一般1-2个term类,Leader就会被选出来。
    Sentinel的选举流程
    Sentinel集群正常运行的时候每个节点epoch相同,当需要故障转移的时候会在集群中选出Leader执行故障转移操作。Sentinel采用了Raft协议实现了Sentinel间选举Leader的算法,不过也不完全跟论文描述的步骤一致。Sentinel集群运行过程中故障转移完成,所有Sentinel又会恢复平等。Leader仅仅是故障转移操作出现的角色。
    选举流程
    1、某个Sentinel认定master客观下线的节点后,该Sentinel会先看看自己有没有投过票,如果自己已经投过票给其他Sentinel了,在2倍故障转移的超时时间自己就不会成为Leader。相当于它是一个Follower。
    2、如果该Sentinel还没投过票,那么它就成为Candidate。
    3、和Raft协议描述的一样,成为Candidate,Sentinel需要完成几件事情
    1)更新故障转移状态为start
    2)当前epoch加1,相当于进入一个新term,在Sentinel中epoch就是Raft协议中的term。
    3)更新自己的超时时间为当前时间随机加上一段时间,随机时间为1s内的随机毫秒数。
    4)向其他节点发送is-master-down-by-addr命令请求投票。命令会带上自己的epoch。
    5)给自己投一票,在Sentinel中,投票的方式是把自己master结构体里的leader和leader_epoch改成投给的Sentinel和它的epoch。
    4、其他Sentinel会收到Candidate的is-master-down-by-addr命令。如果Sentinel当前epoch和Candidate传给他的epoch一样,说明他已经把自己master结构体里的leader和leader_epoch改成其他Candidate,相当于把票投给了其他Candidate。投过票给别的Sentinel后,在当前epoch内自己就只能成为Follower。
    5、Candidate会不断的统计自己的票数,直到他发现认同他成为Leader的票数超过一半而且超过它配置的quorum(quorum可以参考《redis sentinel设计与实现》)。Sentinel比Raft协议增加了quorum,这样一个Sentinel能否当选Leader还取决于它配置的quorum。
    6、如果在一个选举时间内,Candidate没有获得超过一半且超过它配置的quorum的票数,自己的这次选举就失败了。
    7、如果在一个epoch内,没有一个Candidate获得更多的票数。那么等待超过2倍故障转移的超时时间后,Candidate增加epoch重新投票。
    8、如果某个Candidate获得超过一半且超过它配置的quorum的票数,那么它就成为了Leader。
    9、与Raft协议不同,Leader并不会把自己成为Leader的消息发给其他Sentinel。其他Sentinel等待Leader从slave选出master后,检测到新的master正常工作后,就会去掉客观下线的标识,从而不需要进入故障转移流程。
    参考链接
    20.redis 的持久化的机制,aof 和 rdb 的区别。
    RDB 定时快照方式(snapshot): 定时备份,可能会丢失数据
    AOF 基于语句追加方式 只追加写操作
    AOF 持久化和 RDB 持久化的最主要区别在于,前者记录了数据的变更,而后者是保存了数据本身
    21.redis 的集群怎么同步的数据的。
    redis replication redis-migrate-tool等方式
    #搜索
    22.elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段。elasticsearch 的倒排索引是什么。
    ElasticSearch(简称ES)是一个分布式、Restful的搜索及分析服务器,设计用于分布式计算;能够达到实时搜索,稳定,可靠,快速。和Apache Solr一样,它也是基于Lucence的索引服务器,而ElasticSearch对比Solr的优点在于:

    1.轻量级:安装启动方便,下载文件之后一条命令就可以启动。
    2.Schema free:可以向服务器提交任意结构的JSON对象,Solr中使用schema.xml指定了索引结构。
    3.多索引文件支持:使用不同的index参数就能创建另一个索引文件,Solr中需要另行配置。
    4.分布式:Solr Cloud的配置比较复杂。
    

    倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两个部分组成:“单词词典”和“倒排文件”。
    ##elasticsearch 索引数据多了怎么办,如何调优,部署。
    使用bulk API
    初次索引的时候,把 replica 设置为 0
    增大 threadpool.index.queue_size
    增大 indices.memory.index_buffer_size
    增大 index.translog.flush_threshold_ops
    增大 index.translog.sync_interval
    增大 index.engine.robin.refresh_interval
    参考链接
    23.lucence 内部结构是什么
    索引(Index):
    在Lucene中一个索引是放在一个文件夹中的。
    如上图,同一文件夹中的所有的文件构成一个Lucene索引。
    段(Segment):
    一个索引可以包含多个段,段与段之间是独立的,添加新文档可以生成新的段,不同的段可以合并。
    如上图,具有相同前缀文件的属同一个段,图中共三个段 “_0” 和 "_1"和“_2”。
    segments.gen和segments_X是段的元数据文件,也即它们保存了段的属性信息。
    文档(Document):
    文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。
    新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。
    域(Field):
    一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,作者等,都可以保存在不同的域里。
    不同域的索引方式可以不同,在真正解析域的存储的时候,我们会详细解读。
    词(Term):
    词是索引的最小单位,是经过词法分析和语言处理后的字符串。

    7.最后附一遍持续整理的博客

    彩蛋链接

    展开全文
  • Java面试题全集(上)

    万次阅读 多人点赞 2016-12-28 22:56:50
    2013年年底的时候,我看到了网上流传的一个叫做《Java面试题大全》的东西,认真的阅读了以后发现里面的很多题目是重复且没有价值的题目,还有不少的参考答案也是错误的,于是我花了半个月时间对这个所谓的《Java面试...

    2013年年底的时候,我看到了网上流传的一个叫做《Java面试题大全》的东西,认真的阅读了以后发现里面的很多题目是重复且没有价值的题目,还有不少的参考答案也是错误的,于是我花了半个月时间对这个所谓的《Java面试大全》进行了全面的修订并重新发布在我的CSDN博客。在修订的过程中,参照了当时JDK最新版本(Java 7)给出了题目的答案和相关代码,去掉了EJB 2.x、JSF等无用内容或过时内容,补充了数据结构和算法、大型网站技术架构、设计模式、UML、Spring MVC等内容并对很多知识点进行了深入的剖析,例如hashCode方法的设计、垃圾收集、并发编程、数据库事务等。当时我甚至希望把面试中经常出现的操作系统、数据库、软件测试等内容也补充进去,但是由于各种原因,最终只整理出了150道面试题。让我欣慰的是,这150道题还是帮助到了很多人,而且在我CSDN博客上的总访问量超过了5万次,最终还被很多网站和个人以原创的方式转载了。最近一年内,用百度搜索"Java面试"我写的这些东西基本上都排在搜索结果的前5名,这让我觉得"亚历山大",因为我写的这些东西一旦不准确就可能误导很多人。2014年的时候我又整理了30道题,希望把之前遗漏的面试题和知识点补充上去,但是仍然感觉挂一漏万,而且Java 8问世后很多新的东西又需要去总结和整理。为此,我不止一次的修改了之前的180题,修改到自己已经感觉有些疲惫或者厌烦了。2014年至今,自己带的学生又有很多走上了Java程序员、Java工程师的工作岗位,他们的面试经验也还没来得及跟大家分享,冥冥之中似乎有一股力量在刺激我要重新写一篇《Java面试题全集》,于是这篇文章就诞生了。请不要责备我把那些出现过的内容又写了一次,因为每次写东西就算是重复的内容,我也需要对编程语言和相关技术进行重新思考,不仅字斟句酌更是力求至臻完美,所以请相信我分享的一定是更新的、更好的、更有益的东西,这些内容也诉说着一个职业程序员和培训师的思想、精神和情感。

    1、面向对象的特征有哪些方面?
    答:面向对象的特征主要有以下几个方面:
    - 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
    - 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段(如果不能理解请阅读阎宏博士的《Java与模式》或《设计模式精解》中关于桥梁模式的部分)。
    - 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事情)。
    - 多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当A系统访问B系统提供的服务时,B系统有多种提供服务的方式,但一切对A系统来说都是透明的(就像电动剃须刀是A系统,它的供电系统是B系统,B系统可以使用电池供电或者用交流电,甚至还有可能是太阳能,A系统只会通过B类对象调用供电的方法,但并不知道供电系统的底层实现是什么,究竟通过何种方式获得了动力)。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1). 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2). 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。

    2、访问修饰符public,private,protected,以及不写(默认)时的区别?
    答:

    修饰符 当前类 同 包 子 类 其他包
    public
    protected ×
    default × ×
    private × × ×

    类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。Java中,外部类的修饰符只能是public或默认,类的成员(包括内部类)的修饰符可以是以上四种。

    3、String 是最基本的数据类型吗?
    答:不是。Java中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type),Java 5以后引入的枚举类型也算是一种比较特殊的引用类型。

    4、float f=3.4;是否正确?
    答:不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。

    5、short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?
    答:对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋值给short型。而short s1 = 1; s1 += 1;可以正确编译,因为s1+= 1;相当于s1 = (short)(s1 + 1);其中有隐含的强制类型转换。

    6、Java有没有goto?
    答:goto 是Java中的保留字,在目前版本的Java中没有使用。(根据James Gosling(Java之父)编写的《The Java Programming Language》一书的附录中给出了一个Java关键字列表,其中有goto和const,但是这两个是目前无法使用的关键字,因此有些地方将其称之为保留字,其实保留字这个词应该有更广泛的意义,因为熟悉C语言的程序员都知道,在系统类库中使用过的有特殊意义的单词或单词的组合都被视为保留字)

    7、int和Integer有什么区别?
    答:Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
    Java 为每个原始类型提供了包装类型:
    - 原始类型: boolean,char,byte,short,int,long,float,double
    - 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double

    class AutoUnboxingTest {
    
        public static void main(String[] args) {
            Integer a = new Integer(3);
            Integer b = 3;                  // 将3自动装箱成Integer类型
            int c = 3;
            System.out.println(a == b);     // false 两个引用没有引用同一对象
            System.out.println(a == c);     // true a自动拆箱成int类型再和c比较
        }
    }

    最近还遇到一个面试题,也是和自动装箱和拆箱有点关系的,代码如下所示:

    public class Test03 {
    
        public static void main(String[] args) {
            Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
    
            System.out.println(f1 == f2);
            System.out.println(f3 == f4);
        }
    }

    如果不明就里很容易认为两个输出要么都是true要么都是false。首先需要注意的是f1、f2、f3、f4四个变量都是Integer对象引用,所以下面的==运算比较的不是值而是引用。装箱的本质是什么呢?当我们给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,如果看看valueOf的源代码就知道发生了什么。

        public static Integer valueOf(int i) {
            if (i >= IntegerCache.low && i <= IntegerCache.high)
                return IntegerCache.cache[i + (-IntegerCache.low)];
            return new Integer(i);
        }

    IntegerCache是Integer的内部类,其代码如下所示:

    /**
         * Cache to support the object identity semantics of autoboxing for values between
         * -128 and 127 (inclusive) as required by JLS.
         *
         * The cache is initialized on first usage.  The size of the cache
         * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
         * During VM initialization, java.lang.Integer.IntegerCache.high property
         * may be set and saved in the private system properties in the
         * sun.misc.VM class.
         */
    
        private static class IntegerCache {
            static final int low = -128;
            static final int high;
            static final Integer cache[];
    
            static {
                // high value may be configured by property
                int h = 127;
                String integerCacheHighPropValue =
                    sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
                if (integerCacheHighPropValue != null) {
                    try {
                        int i = parseInt(integerCacheHighPropValue);
                        i = Math.max(i, 127);
                        // Maximum array size is Integer.MAX_VALUE
                        h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                    } catch( NumberFormatException nfe) {
                        // If the property cannot be parsed into an int, ignore it.
                    }
                }
                high = h;
    
                cache = new Integer[(high - low) + 1];
                int j = low;
                for(int k = 0; k < cache.length; k++)
                    cache[k] = new Integer(j++);
    
                // range [-128, 127] must be interned (JLS7 5.1.7)
                assert IntegerCache.high >= 127;
            }
    
            private IntegerCache() {}
        }

    简单的说,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1==f2的结果是true,而f3==f4的结果是false。

    提醒:越是貌似简单的面试题其中的玄机就越多,需要面试者有相当深厚的功力。

    8、&和&&的区别?
    答:&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是null而且不是空字符串,应当写为:username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。

    补充:如果你熟悉JavaScript,那你可能更能感受到短路运算的强大,想成为JavaScript的高手就先从玩转短路运算开始吧。

    9、解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法。
    答:通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间;而通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的100、"hello"和常量都是放在常量池中,常量池是方法区的一部分,。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和常量池空间不足则会引发OutOfMemoryError。

    String str = new String("hello");

    上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。

    补充1:较新版本的Java(从Java 6的某个更新开始)中,由于JIT编译器的发展和"逃逸分析"技术的逐渐成熟,栈上分配、标量替换等优化技术使得对象一定分配在堆上这件事情已经变得不那么绝对了。

    补充2:运行时常量池相当于Class文件常量池具有动态性,Java语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String类的intern()方法就是这样的。

    看看下面代码的执行结果是什么并且比较一下Java 7以前和以后的运行结果是否一致。

    String s1 = new StringBuilder("go")
        .append("od").toString();
    System.out.println(s1.intern() == s1);
    String s2 = new StringBuilder("ja")
        .append("va").toString();
    System.out.println(s2.intern() == s2);

    10、Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?
    答:Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加0.5然后进行下取整。

    11、switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上?
    答:在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。从Java 5开始,Java中引入了枚举类型,expr也可以是enum类型,从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。

    12、用最有效率的方法计算2乘以8?
    答: 2 << 3(左移3位相当于乘以2的3次方,右移3位相当于除以2的3次方)。

    补充:我们为编写的类重写hashCode方法时,可能会看到如下所示的代码,其实我们不太理解为什么要使用这样的乘法运算来产生哈希码(散列码),而且为什么这个数是个素数,为什么通常选择31这个数?前两个问题的答案你可以自己百度一下,选择31是因为可以用移位和减法运算来代替乘法,从而得到更好的性能。说到这里你可能已经想到了:31 * num 等价于(num << 5) - num,左移5位相当于乘以2的5次方再减去自身就相当于乘以31,现在的VM都能自动完成这个优化。

    public class PhoneNumber {
        private int areaCode;
        private String prefix;
        private String lineNumber;
    
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + areaCode;
            result = prime * result
                    + ((lineNumber == null) ? 0 : lineNumber.hashCode());
            result = prime * result + ((prefix == null) ? 0 : prefix.hashCode());
            return result;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            PhoneNumber other = (PhoneNumber) obj;
            if (areaCode != other.areaCode)
                return false;
            if (lineNumber == null) {
                if (other.lineNumber != null)
                    return false;
            } else if (!lineNumber.equals(other.lineNumber))
                return false;
            if (prefix == null) {
                if (other.prefix != null)
                    return false;
            } else if (!prefix.equals(other.prefix))
                return false;
            return true;
        }
    
    }

    13、数组有没有length()方法?String有没有length()方法?
    答:数组没有length()方法,有length 的属性。String 有length()方法。JavaScript中,获得字符串的长度是通过length属性得到的,这一点容易和Java混淆。

    14、在Java中,如何跳出当前的多重嵌套循环?
    答:在最外层循环前加一个标记如A,然后用break A;可以跳出多重循环。(Java中支持带标签的break和continue语句,作用有点类似于C和C++中的goto语句,但是就像要避免使用goto一样,应该避免使用带标签的break和continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法其实不知道更好)

    15、构造器(constructor)是否可被重写(override)?
    答:构造器不能被继承,因此不能被重写,但可以被重载。

    16、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?
    答:不对,如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。Java对于eqauls方法和hashCode方法是这样规定的:(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;(2)如果两个对象的hashCode相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。

    补充:关于equals和hashCode方法,很多Java程序都知道,但很多人也就是仅仅知道而已,在Joshua Bloch的大作《Effective Java》(很多软件公司,《Effective Java》、《Java编程思想》以及《重构:改善既有代码质量》是Java程序员必看书籍,如果你还没看过,那就赶紧去亚马逊买一本吧)中是这样介绍equals方法的:首先equals方法必须满足自反性(x.equals(x)必须返回true)、对称性(x.equals(y)返回true时,y.equals(x)也必须返回true)、传递性(x.equals(y)和y.equals(z)都返回true时,x.equals(z)也必须返回true)和一致性(当x和y引用的对象信息没有被修改时,多次调用x.equals(y)应该得到同样的返回值),而且对于任何非null值的引用x,x.equals(null)必须返回false。实现高质量的equals方法的诀窍包括:1. 使用==操作符检查"参数是否为这个对象的引用";2. 使用instanceof操作符检查"参数是否为正确的类型";3. 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;4. 编写完equals方法后,问自己它是否满足对称性、传递性、一致性;5. 重写equals时总是要重写hashCode;6. 不要将equals方法参数中的Object对象替换为其他的类型,在重写时不要忘掉@Override注解。

    17、是否可以继承String类?
    答:String 类是final类,不可以被继承。

    补充:继承String本身就是一个错误的行为,对String类型最好的重用方式是关联关系(Has-A)和依赖关系(Use-A)而不是继承关系(Is-A)。

    18、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
    答:是值传递。Java语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++和C#中可以通过传引用或传输出参数来改变传入的参数的值。在C#中可以编写如下所示的代码,但是在Java中却做不到。

    using System;
    
    namespace CS01 {
    
        class Program {
            public static void swap(ref int x, ref int y) {
                int temp = x;
                x = y;
                y = temp;
            }
    
            public static void Main (string[] args) {
                int a = 5, b = 10;
                swap (ref a, ref b);
                // a = 10, b = 5;
                Console.WriteLine ("a = {0}, b = {1}", a, b);
            }
        }
    }

    说明:Java中没有传引用实在是非常的不方便,这一点在Java 8中仍然没有得到改进,正是如此在Java编写的代码中才会出现大量的Wrapper类(将需要通过方法调用修改的引用置于一个Wrapper类中,再将Wrapper对象传入方法),这样的做法只会让代码变得臃肿,尤其是让从C和C++转型为Java程序员的开发者无法容忍。

    19、String和StringBuilder、StringBuffer的区别?
    答:Java平台提供了两种类型的字符串:String和StringBuffer/StringBuilder,它们可以储存和操作字符串。其中String是只读字符串,也就意味着String引用的字符串内容是不能被改变的。而StringBuffer/StringBuilder类表示的字符串对象可以直接进行修改。StringBuilder是Java 5中引入的,它和StringBuffer的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被synchronized修饰,因此它的效率也比StringBuffer要高。

    面试题1 - 什么情况下用+运算符进行字符串连接比调用StringBuffer/StringBuilder对象的append方法连接字符串性能更好?

    面试题2 - 请说出下面程序的输出。

    class StringEqualTest {
    
        public static void main(String[] args) {
            String s1 = "Programming";
            String s2 = new String("Programming");
            String s3 = "Program";
            String s4 = "ming";
            String s5 = "Program" + "ming";
            String s6 = s3 + s4;
            System.out.println(s1 == s2);
            System.out.println(s1 == s5);
            System.out.println(s1 == s6);
            System.out.println(s1 == s6.intern());
            System.out.println(s2 == s2.intern());
        }
    }

    补充:解答上面的面试题需要清除两点:1. String对象的intern方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与String对象的equals结果是true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;2. 字符串的+操作其本质是创建了StringBuilder对象进行append操作,然后将拼接后的StringBuilder对象用toString方法处理成String对象,这一点可以用javap -c StringEqualTest.class命令获得class文件对应的JVM字节码指令就可以看出来。

    20、重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
    答:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。

    面试题:华为的面试题中曾经问过这样一个问题 - "为什么不能根据返回类型来区分重载",快说出你的答案吧!

    21、描述一下JVM加载class文件的原理机制?
    答:JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
    由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。
    类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:

    • Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
    • Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
    • System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。

    22、char 型变量中能不能存贮一个中文汉字,为什么?
    答:char类型可以存储一个中文汉字,因为Java中使用的编码是Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个char类型占2个字节(16比特),所以放一个中文是没问题的。

    补充:使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以Java中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader和OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于C程序员来说,要完成这样的编码转换恐怕要依赖于union(联合体/共用体)共享内存的特征来实现了。

    23、抽象类(abstract class)和接口(interface)有什么异同?
    答:抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。抽象类中的成员可以是private、默认、protected、public的,而接口中的成员全都是public的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。

    24、静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?
    答:Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化,其语法看起来挺诡异的,如下所示。

    /**
     * 扑克类(一副扑克)
     * @author 骆昊
     *
     */
    public class Poker {
        private static String[] suites = {"黑桃", "红桃", "草花", "方块"};
        private static int[] faces = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
    
        private Card[] cards;
    
        /**
         * 构造器
         * 
         */
        public Poker() {
            cards = new Card[52];
            for(int i = 0; i < suites.length; i++) {
                for(int j = 0; j < faces.length; j++) {
                    cards[i * 13 + j] = new Card(suites[i], faces[j]);
                }
            }
        }
    
        /**
         * 洗牌 (随机乱序)
         * 
         */
        public void shuffle() {
            for(int i = 0, len = cards.length; i < len; i++) {
                int index = (int) (Math.random() * len);
                Card temp = cards[index];
                cards[index] = cards[i];
                cards[i] = temp;
            }
        }
    
        /**
         * 发牌
         * @param index 发牌的位置
         * 
         */
        public Card deal(int index) {
            return cards[index];
        }
    
        /**
         * 卡片类(一张扑克)
         * [内部类]
         * @author 骆昊
         *
         */
        public class Card {
            private String suite;   // 花色
            private int face;       // 点数
    
            public Card(String suite, int face) {
                this.suite = suite;
                this.face = face;
            }
    
            @Override
            public String toString() {
                String faceStr = "";
                switch(face) {
                case 1: faceStr = "A"; break;
                case 11: faceStr = "J"; break;
                case 12: faceStr = "Q"; break;
                case 13: faceStr = "K"; break;
                default: faceStr = String.valueOf(face);
                }
                return suite + faceStr;
            }
        }
    }

    测试代码:

    class PokerTest {
    
        public static void main(String[] args) {
            Poker poker = new Poker();
            poker.shuffle();                // 洗牌
            Poker.Card c1 = poker.deal(0);  // 发第一张牌
            // 对于非静态内部类Card
            // 只有通过其外部类Poker对象才能创建Card对象
            Poker.Card c2 = poker.new Card("红心", 1);    // 自己创建一张牌
    
            System.out.println(c1);     // 洗牌后的第一张
            System.out.println(c2);     // 打印: 红心A
        }
    }

    面试题 - 下面的代码哪些地方会产生编译错误?

    class Outer {
    
        class Inner {}
    
        public static void foo() { new Inner(); }
    
        public void bar() { new Inner(); }
    
        public static void main(String[] args) {
            new Inner();
        }
    }

    注意:Java中非静态内部类对象的创建要依赖其外部类对象,上面的面试题中foo和main方法都是静态方法,静态方法中没有this,也就是说没有所谓的外部类对象,因此无法创建内部类对象,如果要在静态方法中创建内部类对象,可以这样做:

        new Outer().new Inner();

    25、Java 中会存在内存泄漏吗,请简单描述。
    答:理论上Java因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。例如Hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。下面例子中的代码也会导致内存泄露。

    import java.util.Arrays;
    import java.util.EmptyStackException;
    
    public class MyStack<T> {
        private T[] elements;
        private int size = 0;
    
        private static final int INIT_CAPACITY = 16;
    
        public MyStack() {
            elements = (T[]) new Object[INIT_CAPACITY];
        }
    
        public void push(T elem) {
            ensureCapacity();
            elements[size++] = elem;
        }
    
        public T pop() {
            if(size == 0) 
                throw new EmptyStackException();
            return elements[--size];
        }
    
        private void ensureCapacity() {
            if(elements.length == size) {
                elements = Arrays.copyOf(elements, 2 * size + 1);
            }
        }
    }

    上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明显的问题,它甚至可以通过你编写的各种单元测试。然而其中的pop方法却存在内存泄露的问题,当我们用pop方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过期引用(obsolete reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的,这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发Disk Paging(物理内存与硬盘的虚拟内存交换数据),甚至造成OutOfMemoryError。

    26、抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被synchronized修饰?
    答:都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。

    27、阐述静态变量和实例变量的区别。
    答:静态变量是被static修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。

    补充:在Java开发中,上下文类和工具类中通常会有大量的静态成员。

    28、是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
    答:不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化。

    29、如何实现对象克隆?
    答:有两种方式:
      1). 实现Cloneable接口并重写Object类中的clone()方法;
      2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。

    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    
    public class MyUtil {
    
        private MyUtil() {
            throw new AssertionError();
        }
    
        @SuppressWarnings("unchecked")
        public static <T extends Serializable> T clone(T obj) throws Exception {
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bout);
            oos.writeObject(obj);
    
            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bin);
            return (T) ois.readObject();
    
            // 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义
            // 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
        }
    }

    下面是测试代码:

    import java.io.Serializable;
    
    /**
     * 人类
     * @author 骆昊
     *
     */
    class Person implements Serializable {
        private static final long serialVersionUID = -9102017020286042305L;
    
        private String name;    // 姓名
        private int age;        // 年龄
        private Car car;        // 座驾
    
        public Person(String name, int age, Car car) {
            this.name = name;
            this.age = age;
            this.car = car;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public Car getCar() {
            return car;
        }
    
        public void setCar(Car car) {
            this.car = car;
        }
    
        @Override
        public String toString() {
            return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
        }
    
    }
    /**
     * 小汽车类
     * @author 骆昊
     *
     */
    class Car implements Serializable {
        private static final long serialVersionUID = -5713945027627603702L;
    
        private String brand;       // 品牌
        private int maxSpeed;       // 最高时速
    
        public Car(String brand, int maxSpeed) {
            this.brand = brand;
            this.maxSpeed = maxSpeed;
        }
    
        public String getBrand() {
            return brand;
        }
    
        public void setBrand(String brand) {
            this.brand = brand;
        }
    
        public int getMaxSpeed() {
            return maxSpeed;
        }
    
        public void setMaxSpeed(int maxSpeed) {
            this.maxSpeed = maxSpeed;
        }
    
        @Override
        public String toString() {
            return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
        }
    
    }
    class CloneTest {
    
        public static void main(String[] args) {
            try {
                Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300));
                Person p2 = MyUtil.clone(p1);   // 深度克隆
                p2.getCar().setBrand("BYD");
                // 修改克隆的Person对象p2关联的汽车对象的品牌属性
                // 原来的Person对象p1关联的汽车不会受到任何影响
                // 因为在克隆Person对象时其关联的汽车对象也被克隆了
                System.out.println(p1);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。

    30、GC是什么?为什么要有GC?
    答:GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。
    垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。

    补充:垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的Java进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要创建的对象。Java平台对堆内存回收和再利用的基本算法被称为标记和清除,但是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟Java对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域:
    - 伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的区域。
    - 幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。
    - 终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。

    与垃圾回收相关的JVM参数:

    • -Xms / -Xmx — 堆的初始大小 / 堆的最大大小
    • -Xmn — 堆中年轻代的大小
    • -XX:-DisableExplicitGC — 让System.gc()不产生任何作用
    • -XX:+PrintGCDetails — 打印GC的细节
    • -XX:+PrintGCDateStamps — 打印GC操作的时间戳
    • -XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
    • -XX:NewRatio — 可以设置老生代和新生代的比例
    • -XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
    • -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
    • -XX:TargetSurvivorRatio:设置幸存区的目标使用率

    31、String s = new String("xyz");创建了几个字符串对象?
    答:两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。

    32、接口是否可继承(extends)接口?抽象类是否可实现(implements)接口?抽象类是否可继承具体类(concrete class)?
    答:接口可以继承接口,而且支持多重继承。抽象类可以实现(implements)接口,抽象类可继承具体类也可以继承抽象类。

    33、一个".java"源文件中是否可以包含多个类(不是内部类)?有什么限制?
    答:可以,但一个源文件中最多只能有一个公开类(public class)而且文件名必须和公开类的类名完全保持一致。

    34、Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
    答:可以继承其他类或实现其他接口,在Swing编程和Android开发中常用此方式来实现事件监听和回调。

    35、内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
    答:一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。

    36、Java 中的final关键字有哪些用法?
    答:(1)修饰类:表示该类不能被继承;(2)修饰方法:表示方法不能被重写;(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。

    37、指出下面程序的运行结果。

    class A {
    
        static {
            System.out.print("1");
        }
    
        public A() {
            System.out.print("2");
        }
    }
    
    class B extends A{
    
        static {
            System.out.print("a");
        }
    
        public B() {
            System.out.print("b");
        }
    }
    
    public class Hello {
    
        public static void main(String[] args) {
            A ab = new B();
            ab = new B();
        }
    
    }

    答:执行结果:1a2b2b。创建对象时构造器的调用顺序是:先初始化静态成员,然后调用父类构造器,再初始化非静态成员,最后调用自身构造器。

    提示:如果不能给出此题的正确答案,说明之前第21题Java类加载机制还没有完全理解,赶紧再看看吧。

    38、数据类型之间的转换:
    - 如何将字符串转换为基本数据类型?
    - 如何将基本数据类型转换为字符串?
    答:
    - 调用基本数据类型对应的包装类中的方法parseXXX(String)或valueOf(String)即可返回相应基本类型;
    - 一种方法是将基本数据类型与空字符串("")连接(+)即可获得其所对应的字符串;另一种方法是调用String 类中的valueOf()方法返回相应字符串

    39、如何实现字符串的反转及替换?
    答:方法很多,可以自己写实现也可以使用String或StringBuffer/StringBuilder中的方法。有一道很常见的面试题是用递归实现字符串反转,代码如下所示:

        public static String reverse(String originStr) {
            if(originStr == null || originStr.length() <= 1) 
                return originStr;
            return reverse(originStr.substring(1)) + originStr.charAt(0);
        }

    40、怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串?
    答:代码如下所示:

    String s1 = "你好";
    String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");

    41、日期和时间:
    - 如何取得年月日、小时分钟秒?
    - 如何取得从1970年1月1日0时0分0秒到现在的毫秒数?
    - 如何取得某月的最后一天?
    - 如何格式化日期?
    答:
    问题1:创建java.util.Calendar 实例,调用其get()方法传入不同的参数即可获得参数所对应的值。Java 8中可以使用java.time.LocalDateTimel来获取,代码如下所示。

    public class DateTimeTest {
        public static void main(String[] args) {
            Calendar cal = Calendar.getInstance();
            System.out.println(cal.get(Calendar.YEAR));
            System.out.println(cal.get(Calendar.MONTH));    // 0 - 11
            System.out.println(cal.get(Calendar.DATE));
            System.out.println(cal.get(Calendar.HOUR_OF_DAY));
            System.out.println(cal.get(Calendar.MINUTE));
            System.out.println(cal.get(Calendar.SECOND));
    
            // Java 8
            LocalDateTime dt = LocalDateTime.now();
            System.out.println(dt.getYear());
            System.out.println(dt.getMonthValue());     // 1 - 12
            System.out.println(dt.getDayOfMonth());
            System.out.println(dt.getHour());
            System.out.println(dt.getMinute());
            System.out.println(dt.getSecond());
        }
    }

    问题2:以下方法均可获得该毫秒数。

    Calendar.getInstance().getTimeInMillis();
    System.currentTimeMillis();
    Clock.systemDefaultZone().millis(); // Java 8

    问题3:代码如下所示。

    Calendar time = Calendar.getInstance();
    time.getActualMaximum(Calendar.DAY_OF_MONTH);

    问题4:利用java.text.DataFormat 的子类(如SimpleDateFormat类)中的format(Date)方法可将日期格式化。Java 8中可以用java.time.format.DateTimeFormatter来格式化时间日期,代码如下所示。

    import java.text.SimpleDateFormat;
    import java.time.LocalDate;
    import java.time.format.DateTimeFormatter;
    import java.util.Date;
    
    class DateFormatTest {
    
        public static void main(String[] args) {
            SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");
            Date date1 = new Date();
            System.out.println(oldFormatter.format(date1));
    
            // Java 8
            DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
            LocalDate date2 = LocalDate.now();
            System.out.println(date2.format(newFormatter));
        }
    }

    补充:Java的时间日期API一直以来都是被诟病的东西,为了解决这一问题,Java 8中引入了新的时间日期API,其中包括LocalDate、LocalTime、LocalDateTime、Clock、Instant等类,这些的类的设计都使用了不变模式,因此是线程安全的设计。如果不理解这些内容,可以参考我的另一篇文章《关于Java并发编程的总结和思考》

    42、打印昨天的当前时刻。
    答:

    import java.util.Calendar;
    
    class YesterdayCurrent {
        public static void main(String[] args){
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.DATE, -1);
            System.out.println(cal.getTime());
        }
    }

    在Java 8中,可以用下面的代码实现相同的功能。

    import java.time.LocalDateTime;
    
    class YesterdayCurrent {
    
        public static void main(String[] args) {
            LocalDateTime today = LocalDateTime.now();
            LocalDateTime yesterday = today.minusDays(1);
    
            System.out.println(yesterday);
        }
    }

    43、比较一下Java和JavaSciprt。
    答:JavaScript 与Java是两个公司开发的不同的两个产品。Java 是原Sun Microsystems公司推出的面向对象的程序设计语言,特别适合于互联网应用程序开发;而JavaScript是Netscape公司的产品,为了扩展Netscape浏览器的功能而开发的一种可以嵌入Web页面中运行的基于对象和事件驱动的解释性语言。JavaScript的前身是LiveScript;而Java的前身是Oak语言。
    下面对两种语言间的异同作如下比较:
    - 基于对象和面向对象:Java是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言,因而它本身提供了非常丰富的内部对象供设计人员使用。
    - 解释和编译:Java的源代码在执行之前,必须经过编译。JavaScript是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行。(目前的浏览器几乎都使用了JIT(即时编译)技术来提升JavaScript的运行效率)
    - 强类型变量和类型弱变量:Java采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript中变量是弱类型的,甚至在使用变量前可以不作声明,JavaScript的解释器在运行时检查推断其数据类型。
    - 代码格式不一样。

    补充:上面列出的四点是网上流传的所谓的标准答案。其实Java和JavaScript最重要的区别是一个是静态语言,一个是动态语言。目前的编程语言的发展趋势是函数式语言和动态语言。在Java中类(class)是一等公民,而JavaScript中函数(function)是一等公民,因此JavaScript支持函数式编程,可以使用Lambda函数和闭包(closure),当然Java 8也开始支持函数式编程,提供了对Lambda表达式以及函数式接口的支持。对于这类问题,在面试的时候最好还是用自己的语言回答会更加靠谱,不要背网上所谓的标准答案。

    44、什么时候用断言(assert)?
    答:断言在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。一般来说,断言用于保证程序最基本、关键的正确性。断言检查通常在开发和测试时开启。为了保证程序的执行效率,在软件发布后断言检查通常是关闭的。断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为true;如果表达式的值为false,那么系统会报告一个AssertionError。断言的使用如下面的代码所示:

    assert(a > 0); // throws an AssertionError if a <= 0

    断言可以有两种形式:
    assert Expression1;
    assert Expression1 : Expression2 ;
    Expression1 应该总是产生一个布尔值。
    Expression2 可以是得出一个值的任意表达式;这个值用于生成显示更多调试信息的字符串消息。

    要在运行时启用断言,可以在启动JVM时使用-enableassertions或者-ea标记。要在运行时选择禁用断言,可以在启动JVM时使用-da或者-disableassertions标记。要在系统类中启用或禁用断言,可使用-esa或-dsa标记。还可以在包的基础上启用或者禁用断言。

    注意:断言不应该以任何方式改变程序的状态。简单的说,如果希望在不满足某些条件时阻止代码的执行,就可以考虑用断言来阻止它。

    45、Error和Exception有什么区别?
    答:Error表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。

    面试题:2005年摩托罗拉的面试中曾经问过这么一个问题“If a process reports a stack overflow run-time error, what’s the most possible cause?”,给了四个选项a. lack of memory; b. write on an invalid memory space; c. recursive function calling; d. array index out of boundary. Java程序在运行时也可能会遭遇StackOverflowError,这是一个无法恢复的错误,只能重新修改代码了,这个面试题的答案是c。如果写了不能迅速收敛的递归,则很有可能引发栈溢出的错误,如下所示:

    class StackOverflowErrorTest {
    
        public static void main(String[] args) {
            main(null);
        }
    }

    提示:用递归编写程序时一定要牢记两点:1. 递归公式;2. 收敛条件(什么时候就不再继续递归)。

    46、try{}里有一个return语句,那么紧跟在这个try后的finally{}里的代码会不会被执行,什么时候被执行,在return前还是后?
    答:会执行,在方法返回调用者前执行。

    注意:在finally中改变返回值的做法是不好的,因为如果存在finally代码块,try中的return语句不会立马返回调用者,而是记录下返回值待finally代码块执行完毕之后再向调用者返回其值,然后如果在finally中修改了返回值,就会返回修改后的值。显然,在finally中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java中也可以通过提升编译器的语法检查级别来产生警告或错误,Eclipse中可以在如图所示的地方进行设置,强烈建议将此项设置为编译错误。

    这里写图片描述

    47、Java语言如何进行异常处理,关键字:throws、throw、try、catch、finally分别如何使用?
    答:Java通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在Java中,每个异常都是一个对象,它是Throwable类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java的异常处理是通过5个关键词来实现的:try、catch、throw、throws和finally。一般情况下是用try来执行一段程序,如果系统会抛出(throw)一个异常对象,可以通过它的类型来捕获(catch)它,或通过总是执行代码块(finally)来处理;try用来指定一块预防所有异常的程序;catch子句紧跟在try块后面,用来指定你想要捕获的异常的类型;throw语句用来明确地抛出一个异常;throws用来声明一个方法可能抛出的各种异常(当然声明异常时允许无病呻吟);finally为确保一段代码不管发生什么异常状况都要被执行;try语句可以嵌套,每当遇到一个try语句,异常的结构就会被放入异常栈中,直到所有的try语句都完成。如果下一级的try语句没有对某种异常进行处理,异常栈就会执行出栈操作,直到遇到有处理这种异常的try语句或者最终将异常抛给JVM。

    48、运行时异常与受检异常有何异同?
    答:异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样,是面向对象程序设计中经常被滥用的东西,在Effective Java中对异常的使用给出了以下指导原则:
    - 不要将异常处理用于正常的控制流(设计良好的API不应该强迫它的调用者为了正常的控制流而使用异常)
    - 对可以恢复的情况使用受检异常,对编程错误使用运行时异常
    - 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)
    - 优先使用标准的异常
    - 每个方法抛出的异常都要有文档
    - 保持异常的原子性
    - 不要在catch中忽略掉捕获到的异常

    49、列出一些你常见的运行时异常?
    答:
    - ArithmeticException(算术异常)
    - ClassCastException (类转换异常)
    - IllegalArgumentException (非法参数异常)
    - IndexOutOfBoundsException (下标越界异常)
    - NullPointerException (空指针异常)
    - SecurityException (安全异常)

    50、阐述final、finally、finalize的区别。
    答:
    - final:修饰符(关键字)有三种用法:如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。将变量声明为final,可以保证它们在使用中不被改变,被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为final的方法也同样只能使用,不能在子类中被重写。
    - finally:通常放在try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
    - finalize:Object类中定义的方法,Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。

    51、类ExampleA继承Exception,类ExampleB继承ExampleA。
    有如下代码片断:

    try {
        throw new ExampleB("b")
    } catch(ExampleA e){
        System.out.println("ExampleA");
    } catch(Exception e){
        System.out.println("Exception");
    }

    请问执行此段代码的输出是什么?
    答:输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取ExampleA类型异常的catch块能够抓住try块中抛出的ExampleB类型的异常)

    面试题 - 说出下面代码的运行结果。(此题的出处是《Java编程思想》一书)

    class Annoyance extends Exception {}
    class Sneeze extends Annoyance {}
    
    class Human {
    
        public static void main(String[] args) 
            throws Exception {
            try {
                try {
                    throw new Sneeze();
                } 
                catch ( Annoyance a ) {
                    System.out.println("Caught Annoyance");
                    throw a;
                }
            } 
            catch ( Sneeze s ) {
                System.out.println("Caught Sneeze");
                return ;
            }
            finally {
                System.out.println("Hello World!");
            }
        }
    }

    52、List、Set、Map是否继承自Collection接口?
    答:List、Set 是,Map 不是。Map是键值对映射容器,与List和Set有明显的区别,而Set存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List是线性结构的容器,适用于按数值索引访问元素的情形。

    53、阐述ArrayList、Vector、LinkedList的存储性能和特性。
    答:ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,因此已经是Java中的遗留容器。LinkedList使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。Vector属于遗留容器(Java早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),已经不推荐使用,但是由于ArrayList和LinkedListed都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。

    补充:遗留容器中的Properties类和Stack类在设计上有严重的问题,Properties是一个键和值都是字符串的特殊的键值对映射,在设计上应该是关联一个Hashtable并将其两个泛型参数设置为String类型,但是Java API中的Properties直接继承了Hashtable,这很明显是对继承的滥用。这里复用代码的方式应该是Has-A关系而不是Is-A关系,另一方面容器都属于工具类,继承工具类本身就是一个错误的做法,使用工具类最好的方式是Has-A关系(关联)或Use-A关系(依赖)。同理,Stack类继承Vector也是不正确的。Sun公司的工程师们也会犯这种低级错误,让人唏嘘不已。

    54、Collection和Collections的区别?
    答:Collection是一个接口,它是Set、List等容器的父接口;Collections是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。

    55、List、Map、Set三个接口存取元素时,各有什么特点?
    答:List以特定索引来存取元素,可以有重复元素。Set不能存放重复元素(用对象的equals()方法来区分元素是否重复)。Map保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。

    56、TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?
    答:TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)。
    例子1:

    
    public class Student implements Comparable<Student> {
        private String name;        // 姓名
        private int age;            // 年龄
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Student [name=" + name + ", age=" + age + "]";
        }
    
        @Override
        public int compareTo(Student o) {
            return this.age - o.age; // 比较年龄(年龄的升序)
        }
    
    }
    import java.util.Set;
    import java.util.TreeSet;
    
    class Test01 {
    
        public static void main(String[] args) {
            Set<Student> set = new TreeSet<>();     // Java 7的钻石语法(构造器后面的尖括号中不需要写类型)
            set.add(new Student("Hao LUO", 33));
            set.add(new Student("XJ WANG", 32));
            set.add(new Student("Bruce LEE", 60));
            set.add(new Student("Bob YANG", 22));
    
            for(Student stu : set) {
                System.out.println(stu);
            }
    //      输出结果: 
    //      Student [name=Bob YANG, age=22]
    //      Student [name=XJ WANG, age=32]
    //      Student [name=Hao LUO, age=33]
    //      Student [name=Bruce LEE, age=60]
        }
    }

    例子2:

    public class Student {
        private String name;    // 姓名
        private int age;        // 年龄
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        /**
         * 获取学生姓名
         */
        public String getName() {
            return name;
        }
    
        /**
         * 获取学生年龄
         */
        public int getAge() {
            return age;
        }
    
        @Override
        public String toString() {
            return "Student [name=" + name + ", age=" + age + "]";
        }
    
    }
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.List;
    
    class Test02 {
    
        public static void main(String[] args) {
            List<Student> list = new ArrayList<>();     // Java 7的钻石语法(构造器后面的尖括号中不需要写类型)
            list.add(new Student("Hao LUO", 33));
            list.add(new Student("XJ WANG", 32));
            list.add(new Student("Bruce LEE", 60));
            list.add(new Student("Bob YANG", 22));
    
            // 通过sort方法的第二个参数传入一个Comparator接口对象
            // 相当于是传入一个比较对象大小的算法到sort方法中
            // 由于Java中没有函数指针、仿函数、委托这样的概念
            // 因此要将一个算法传入一个方法中唯一的选择就是通过接口回调
            Collections.sort(list, new Comparator<Student> () {
    
                @Override
                public int compare(Student o1, Student o2) {
                    return o1.getName().compareTo(o2.getName());    // 比较学生姓名
                }
            });
    
            for(Student stu : list) {
                System.out.println(stu);
            }
    //      输出结果: 
    //      Student [name=Bob YANG, age=22]
    //      Student [name=Bruce LEE, age=60]
    //      Student [name=Hao LUO, age=33]
    //      Student [name=XJ WANG, age=32]
        }
    }

    57、Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
    答:sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第66题中的线程状态转换图)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

    补充:可能不少人对什么是进程,什么是线程还比较模糊,对于为什么需要多线程编程也不是特别理解。简单的说:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它可能占用了更多的CPU资源。当然,也不是线程越多,程序的性能就越好,因为线程之间的调度和切换也会浪费CPU时间。时下很时髦的Node.js就采用了单线程异步I/O的工作模式。

    58、线程的sleep()方法和yield()方法有什么区别?
    答:
    ① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
    ② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
    ③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
    ④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

    59、当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?
    答:不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

    60、请说出与线程同步以及线程调度相关的方法。
    答:
    - wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
    - sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
    - notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
    - notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

    提示:关于Java多线程和并发编程的问题,建议大家看我的另一篇文章《关于Java并发编程的总结和思考》

    补充:Java 5通过Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;此外,Java 5还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。

    下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。

    • 银行账户类:
    /**
     * 银行账户
     * @author 骆昊
     *
     */
    public class Account {
        private double balance;     // 账户余额
    
        /**
         * 存款
         * @param money 存入金额
         */
        public void deposit(double money) {
            double newBalance = balance + money;
            try {
                Thread.sleep(10);   // 模拟此业务需要一段处理时间
            }
            catch(InterruptedException ex) {
                ex.printStackTrace();
            }
            balance = newBalance;
        }
    
        /**
         * 获得账户余额
         */
        public double getBalance() {
            return balance;
        }
    }
    • 存钱线程类:
    /**
     * 存钱线程
     * @author 骆昊
     *
     */
    public class AddMoneyThread implements Runnable {
        private Account account;    // 存入账户
        private double money;       // 存入金额
    
        public AddMoneyThread(Account account, double money) {
            this.account = account;
            this.money = money;
        }
    
        @Override
        public void run() {
            account.deposit(money);
        }
    
    }
    • 测试类:
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Test01 {
    
        public static void main(String[] args) {
            Account account = new Account();
            ExecutorService service = Executors.newFixedThreadPool(100);
    
            for(int i = 1; i <= 100; i++) {
                service.execute(new AddMoneyThread(account, 1));
            }
    
            service.shutdown();
    
            while(!service.isTerminated()) {}
    
            System.out.println("账户余额: " + account.getBalance());
        }
    }

    在没有同步的情况下,执行结果通常是显示账户余额在10元以下,出现这种状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也能够进入存款的方法中,线程B读取到的账户余额仍然是线程A存入1元钱之前的账户余额,因此也是在原来的余额0上面做了加1元的操作,同理线程C也会做类似的事情,所以最后100个线程执行结束时,本来期望账户余额为100元,但实际得到的通常在10元以下(很可能是1元哦)。解决这个问题的办法就是同步,当一个线程对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行操作,代码有如下几种调整方案:

    • 在银行账户的存款(deposit)方法上同步(synchronized)关键字
    /**
     * 银行账户
     * @author 骆昊
     *
     */
    public class Account {
        private double balance;     // 账户余额
    
        /**
         * 存款
         * @param money 存入金额
         */
        public synchronized void deposit(double money) {
            double newBalance = balance + money;
            try {
                Thread.sleep(10);   // 模拟此业务需要一段处理时间
            }
            catch(InterruptedException ex) {
                ex.printStackTrace();
            }
            balance = newBalance;
        }
    
        /**
         * 获得账户余额
         */
        public double getBalance() {
            return balance;
        }
    }
    • 在线程调用存款方法时对银行账户进行同步
    /**
     * 存钱线程
     * @author 骆昊
     *
     */
    public class AddMoneyThread implements Runnable {
        private Account account;    // 存入账户
        private double money;       // 存入金额
    
        public AddMoneyThread(Account account, double money) {
            this.account = account;
            this.money = money;
        }
    
        @Override
        public void run() {
            synchronized (account) {
                account.deposit(money); 
            }
        }
    
    }
    • 通过Java 5显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 银行账户
     * 
     * @author 骆昊
     *
     */
    public class Account {
        private Lock accountLock = new ReentrantLock();
        private double balance; // 账户余额
    
        /**
         * 存款
         * 
         * @param money
         *            存入金额
         */
        public void deposit(double money) {
            accountLock.lock();
            try {
                double newBalance = balance + money;
                try {
                    Thread.sleep(10); // 模拟此业务需要一段处理时间
                }
                catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                balance = newBalance;
            }
            finally {
                accountLock.unlock();
            }
        }
    
        /**
         * 获得账户余额
         */
        public double getBalance() {
            return balance;
        }
    }

    按照上述三种方式对代码进行修改后,重写执行测试代码Test01,将看到最终的账户余额为100元。当然也可以使用Semaphore或CountdownLatch来实现同步。

    61、编写多线程程序有几种实现方式?
    答:Java 5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。

    补充:Java 5以后创建线程还有第三种方式:实现Callable接口,该接口中的call方法可以在线程执行结束时产生一个返回值,代码如下所示:

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    
    
    class MyTask implements Callable<Integer> {
        private int upperBounds;
    
        public MyTask(int upperBounds) {
            this.upperBounds = upperBounds;
        }
    
        @Override
        public Integer call() throws Exception {
            int sum = 0; 
            for(int i = 1; i <= upperBounds; i++) {
                sum += i;
            }
            return sum;
        }
    
    }
    
    class Test {
    
        public static void main(String[] args) throws Exception {
            List<Future<Integer>> list = new ArrayList<>();
            ExecutorService service = Executors.newFixedThreadPool(10);
            for(int i = 0; i < 10; i++) {
                list.add(service.submit(new MyTask((int) (Math.random() * 100))));
            }
    
            int sum = 0;
            for(Future<Integer> future : list) {
                // while(!future.isDone()) ;
                sum += future.get();
            }
    
            System.out.println(sum);
        }
    }

    62、synchronized关键字的用法?
    答:synchronized关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。在第60题的例子中已经展示了synchronized关键字的用法。

    63、举例说明同步和异步。
    答:如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

    64、启动一个线程是调用run()还是start()方法?
    答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。

    65、什么是线程池(thread pool)?
    答:在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
    Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
    - newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
    - newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
    - newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
    - newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
    - newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

    第60题的例子中演示了通过Executors工具类创建线程池并使用线程池执行线程的代码。如果希望在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来创建线程池,这样能获得更好的性能。

    66、线程的基本状态以及状态之间的关系?
    答:
    这里写图片描述

    说明:其中Running表示运行状态,Runnable表示就绪状态(万事俱备,只欠CPU),Blocked表示阻塞状态,阻塞状态又有多种情况,可能是因为调用wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了sleep()方法或join()方法等待休眠或其他线程结束,或是因为发生了I/O中断。

    67、简述synchronized 和java.util.concurrent.locks.Lock的异同?
    答:Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。

    68、Java中如何实现序列化,有什么意义?
    答:序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。
    要实现序列化,需要让一个类实现Serializable接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆(可以参考第29题)。

    69、Java中有几种类型的流?
    答:字节流和字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer。在java.io 包中还有许多其他的流,主要是为了提高性能和使用方便。关于Java的I/O需要注意的有两点:一是两种对称性(输入和输出的对称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。另外Java中的流不同于C#的是它只有一个维度一个方向。

    面试题 - 编程实现文件拷贝。(这个题目在笔试的时候经常出现,下面的代码给出了两种实现方案)

    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    public final class MyUtil {
    
        private MyUtil() {
            throw new AssertionError();
        }
    
        public static void fileCopy(String source, String target) throws IOException {
            try (InputStream in = new FileInputStream(source)) {
                try (OutputStream out = new FileOutputStream(target)) {
                    byte[] buffer = new byte[4096];
                    int bytesToRead;
                    while((bytesToRead = in.read(buffer)) != -1) {
                        out.write(buffer, 0, bytesToRead);
                    }
                }
            }
        }
    
        public static void fileCopyNIO(String source, String target) throws IOException {
            try (FileInputStream in = new FileInputStream(source)) {
                try (FileOutputStream out = new FileOutputStream(target)) {
                    FileChannel inChannel = in.getChannel();
                    FileChannel outChannel = out.getChannel();
                    ByteBuffer buffer = ByteBuffer.allocate(4096);
                    while(inChannel.read(buffer) != -1) {
                        buffer.flip();
                        outChannel.write(buffer);
                        buffer.clear();
                    }
                }
            }
        }
    }

    注意:上面用到Java 7的TWR,使用TWR后可以不用在finally中释放外部资源 ,从而让代码更加优雅。

    70、写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数。
    答:代码如下:

    import java.io.BufferedReader;
    import java.io.FileReader;
    
    public final class MyUtil {
    
        // 工具类中的方法都是静态方式访问的因此将构造器私有不允许创建对象(绝对好习惯)
        private MyUtil() {
            throw new AssertionError();
        }
    
        /**
         * 统计给定文件中给定字符串的出现次数
         * 
         * @param filename  文件名
         * @param word 字符串
         * @return 字符串在文件中出现的次数
         */
        public static int countWordInFile(String filename, String word) {
            int counter = 0;
            try (FileReader fr = new FileReader(filename)) {
                try (BufferedReader br = new BufferedReader(fr)) {
                    String line = null;
                    while ((line = br.readLine()) != null) {
                        int index = -1;
                        while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) {
                            counter++;
                            line = line.substring(index + word.length());
                        }
                    }
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return counter;
        }
    
    }

    71、如何用Java代码列出一个目录下所有的文件?
    答:
    如果只要求列出当前文件夹下的文件,代码如下所示:

    import java.io.File;
    
    class Test12 {
    
        public static void main(String[] args) {
            File f = new File("/Users/Hao/Downloads");
            for(File temp : f.listFiles()) {
                if(temp.isFile()) {
                    System.out.println(temp.getName());
                }
            }
        }
    }

    如果需要对文件夹继续展开,代码如下所示:

    import java.io.File;
    
    class Test12 {
    
        public static void main(String[] args) {
            showDirectory(new File("/Users/Hao/Downloads"));
        }
    
        public static void showDirectory(File f) {
            _walkDirectory(f, 0);
        }
    
        private static void _walkDirectory(File f, int level) {
            if(f.isDirectory()) {
                for(File temp : f.listFiles()) {
                    _walkDirectory(temp, level + 1);
                }
            }
            else {
                for(int i = 0; i < level - 1; i++) {
                    System.out.print("\t");
                }
                System.out.println(f.getName());
            }
        }
    }

    在Java 7中可以使用NIO.2的API来做同样的事情,代码如下所示:

    class ShowFileTest {
    
        public static void main(String[] args) throws IOException {
            Path initPath = Paths.get("/Users/Hao/Downloads");
            Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() {
    
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
                        throws IOException {
                    System.out.println(file.getFileName().toString());
                    return FileVisitResult.CONTINUE;
                }
    
            });
        }
    }

    72、用Java的套接字编程实现一个多线程的回显(echo)服务器。
    答:

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class EchoServer {
    
        private static final int ECHO_SERVER_PORT = 6789;
    
        public static void main(String[] args) {        
            try(ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) {
                System.out.println("服务器已经启动...");
                while(true) {
                    Socket client = server.accept();
                    new Thread(new ClientHandler(client)).start();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private static class ClientHandler implements Runnable {
            private Socket client;
    
            public ClientHandler(Socket client) {
                this.client = client;
            }
    
            @Override
            public void run() {
                try(BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
                        PrintWriter pw = new PrintWriter(client.getOutputStream())) {
                    String msg = br.readLine();
                    System.out.println("收到" + client.getInetAddress() + "发送的: " + msg);
                    pw.println(msg);
                    pw.flush();
                } catch(Exception ex) {
                    ex.printStackTrace();
                } finally {
                    try {
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    }

    注意:上面的代码使用了Java 7的TWR语法,由于很多外部资源类都间接的实现了AutoCloseable接口(单方法回调接口),因此可以利用TWR语法在try结束的时候通过回调的方式自动调用外部资源类的close()方法,避免书写冗长的finally代码块。此外,上面的代码用一个静态内部类实现线程的功能,使用多线程可以避免一个用户I/O操作所产生的中断影响其他用户对服务器的访问,简单的说就是一个用户的输入操作不会造成其他用户的阻塞。当然,上面的代码使用线程池可以获得更好的性能,因为频繁的创建和销毁线程所造成的开销也是不可忽视的。

    下面是一段回显客户端测试代码:

    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.Socket;
    import java.util.Scanner;
    
    public class EchoClient {
    
        public static void main(String[] args) throws Exception {
            Socket client = new Socket("localhost", 6789);
            Scanner sc = new Scanner(System.in);
            System.out.print("请输入内容: ");
            String msg = sc.nextLine();
            sc.close();
            PrintWriter pw = new PrintWriter(client.getOutputStream());
            pw.println(msg);
            pw.flush();
            BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
            System.out.println(br.readLine());
            client.close();
        }
    }

    如果希望用NIO的多路复用套接字实现服务器,代码如下所示。NIO的操作虽然带来了更好的性能,但是有些操作是比较底层的,对于初学者来说还是有些难于理解。

    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.CharBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    
    public class EchoServerNIO {
    
        private static final int ECHO_SERVER_PORT = 6789;
        private static final int ECHO_SERVER_TIMEOUT = 5000;
        private static final int BUFFER_SIZE = 1024;
    
        private static ServerSocketChannel serverChannel = null;
        private static Selector selector = null;    // 多路复用选择器
        private static ByteBuffer buffer = null;    // 缓冲区
    
        public static void main(String[] args) {
            init();
            listen();
        }
    
        private static void init() {
            try {
                serverChannel = ServerSocketChannel.open();
                buffer = ByteBuffer.allocate(BUFFER_SIZE);
                serverChannel.socket().bind(new InetSocketAddress(ECHO_SERVER_PORT));
                serverChannel.configureBlocking(false);
                selector = Selector.open();
                serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        private static void listen() {
            while (true) {
                try {
                    if (selector.select(ECHO_SERVER_TIMEOUT) != 0) {
                        Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                        while (it.hasNext()) {
                            SelectionKey key = it.next();
                            it.remove();
                            handleKey(key);
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
        private static void handleKey(SelectionKey key) throws IOException {
            SocketChannel channel = null;
    
            try {
                if (key.isAcceptable()) {
                    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                    channel = serverChannel.accept();
                    channel.configureBlocking(false);
                    channel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    channel = (SocketChannel) key.channel();
                    buffer.clear();
                    if (channel.read(buffer) > 0) {
                        buffer.flip();
                        CharBuffer charBuffer = CharsetHelper.decode(buffer);
                        String msg = charBuffer.toString();
                        System.out.println("收到" + channel.getRemoteAddress() + "的消息:" + msg);
                        channel.write(CharsetHelper.encode(CharBuffer.wrap(msg)));
                    } else {
                        channel.close();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                if (channel != null) {
                    channel.close();
                }
            }
        }
    
    }
    import java.nio.ByteBuffer;
    import java.nio.CharBuffer;
    import java.nio.charset.CharacterCodingException;
    import java.nio.charset.Charset;
    import java.nio.charset.CharsetDecoder;
    import java.nio.charset.CharsetEncoder;
    
    public final class CharsetHelper {
        private static final String UTF_8 = "UTF-8";
        private static CharsetEncoder encoder = Charset.forName(UTF_8).newEncoder();
        private static CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();
    
        private CharsetHelper() {
        }
    
        public static ByteBuffer encode(CharBuffer in) throws CharacterCodingException{
            return encoder.encode(in);
        }
    
        public static CharBuffer decode(ByteBuffer in) throws CharacterCodingException{
            return decoder.decode(in);
        }
    }

    73、XML文档定义有几种形式?它们之间有何本质区别?解析XML文档有哪几种方式?
    答:XML文档定义分为DTD和Schema两种形式,二者都是对XML语法的约束,其本质区别在于Schema本身也是一个XML文件,可以被XML解析器解析,而且可以为XML承载的数据定义类型,约束能力较之DTD更强大。对XML的解析主要有DOM(文档对象模型,Document Object Model)、SAX(Simple API for XML)和StAX(Java 6中引入的新的解析XML的方式,Streaming API for XML),其中DOM处理大型文件时其性能下降的非常厉害,这个问题是由DOM树结构占用的内存较多造成的,而且DOM解析方式必须在解析文件之前把整个文档装入内存,适合对XML的随机访问(典型的用空间换取时间的策略);SAX是事件驱动型的XML解析方式,它顺序读取XML文件,不需要一次全部装载整个文件。当遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件,用户通过事件回调代码来处理XML文件,适合对XML的顺序访问;顾名思义,StAX把重点放在流上,实际上StAX与其他解析方式的本质区别就在于应用程序能够把XML作为一个事件流来处理。将XML作为一组事件来处理的想法并不新颖(SAX就是这样做的),但不同之处在于StAX允许应用程序代码把这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。

    74、你在项目中哪些地方用到了XML?
    答:XML的主要作用有两个方面:数据交换和信息配置。在做数据交换时,XML将数据用标签组装成起来,然后压缩打包加密后通过网络传送给接收者,接收解密与解压缩后再从XML文件中还原相关信息进行处理,XML曾经是异构系统间交换数据的事实标准,但此项功能几乎已经被JSON(JavaScript Object Notation)取而代之。当然,目前很多软件仍然使用XML来存储配置信息,我们在很多项目中通常也会将作为配置信息的硬代码写在XML文件中,Java的很多框架也是这么做的,而且这些框架都选择了dom4j作为处理XML的工具,因为Sun公司的官方API实在不怎么好用。

    补充:现在有很多时髦的软件(如Sublime)已经开始将配置文件书写成JSON格式,我们已经强烈的感受到XML的另一项功能也将逐渐被业界抛弃。

    75、阐述JDBC操作数据库的步骤。
    答:下面的代码以连接本机的Oracle数据库为例,演示JDBC操作数据库的步骤。

    • 加载驱动。
        Class.forName("oracle.jdbc.driver.OracleDriver");
    • 创建连接。
        Connection con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", "scott", "tiger");
    • 创建语句。
        PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?");
        ps.setInt(1, 1000);
        ps.setInt(2, 3000);
    • 执行语句。
        ResultSet rs = ps.executeQuery();
    • 处理结果。
        while(rs.next()) {
            System.out.println(rs.getInt("empno") + " - " + rs.getString("ename"));
        }
    • 关闭资源。
        finally {
            if(con != null) {
                try {
                    con.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

    提示:关闭外部资源的顺序应该和打开的顺序相反,也就是说先关闭ResultSet、再关闭Statement、在关闭Connection。上面的代码只关闭了Connection(连接),虽然通常情况下在关闭连接时,连接上创建的语句和打开的游标也会关闭,但不能保证总是如此,因此应该按照刚才说的顺序分别关闭。此外,第一步加载驱动在JDBC 4.0中是可以省略的(自动从类路径中加载驱动),但是我们建议保留。

    76、Statement和PreparedStatement有什么区别?哪个性能更好?
    答:与Statement相比,①PreparedStatement接口代表预编译的语句,它主要的优势在于可以减少SQL的编译错误并增加SQL的安全性(减少SQL注射攻击的可能性);②PreparedStatement中的SQL语句是可以带参数的,避免了用字符串连接拼接SQL语句的麻烦和不安全;③当批量处理SQL或频繁执行相同的查询时,PreparedStatement有明显的性能上的优势,由于数据库可以将编译优化后的SQL语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成执行计划)。

    补充:为了提供对存储过程的调用,JDBC API中还提供了CallableStatement接口。存储过程(Stored Procedure)是数据库中一组为了完成特定功能的SQL语句的集合,经编译后存储在数据库中,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。虽然调用存储过程会在网络开销、安全性、性能上获得很多好处,但是存在如果底层数据库发生迁移时就会有很多麻烦,因为每种数据库的存储过程在书写上存在不少的差别。

    77、使用JDBC操作数据库时,如何提升读取数据的性能?如何提升更新数据的性能?
    答:要提升读取数据的性能,可以指定通过结果集(ResultSet)对象的setFetchSize()方法指定每次抓取的记录数(典型的空间换时间策略);要提升更新数据的性能可以使用PreparedStatement语句构建批处理,将若干SQL语句置于一个批处理中执行。

    78、在进行数据库编程时,连接池有什么作用?
    答:由于创建连接和释放连接都有很大的开销(尤其是数据库服务器不在本地时,每次建立连接都需要进行TCP的三次握手,释放连接需要进行TCP四次握手,造成的开销是不可忽视的),为了提升系统访问数据库的性能,可以事先创建若干连接置于连接池中,需要时直接从连接池获取,使用结束时归还连接池而不必关闭连接,从而避免频繁创建和释放连接所造成的开销,这是典型的用空间换取时间的策略(浪费了空间存储连接,但节省了创建和释放连接的时间)。池化技术在Java开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于Java的开源数据库连接池主要有:C3P0ProxoolDBCPBoneCPDruid等。

    补充:在计算机系统中时间和空间是不可调和的矛盾,理解这一点对设计满足性能要求的算法是至关重要的。大型网站性能优化的一个关键就是使用缓存,而缓存跟上面讲的连接池道理非常类似,也是使用空间换时间的策略。可以将热点数据置于缓存中,当用户查询这些数据时可以直接从缓存中得到,这无论如何也快过去数据库中查询。当然,缓存的置换策略等也会对系统性能产生重要影响,对于这个问题的讨论已经超出了这里要阐述的范围。

    79、什么是DAO模式?
    答:DAO(Data Access Object)顾名思义是一个为数据库或其他持久化机制提供了抽象接口的对象,在不暴露底层持久化方案实现细节的前提下提供了各种数据访问操作。在实际的开发中,应该将所有对数据源的访问操作进行抽象化后封装在一个公共API中。用程序设计语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储。DAO模式实际上包含了两个模式,一是Data Accessor(数据访问器),二是Data Object(数据对象),前者要解决如何访问数据的问题,而后者要解决的是如何用对象封装数据。

    80、事务的ACID是指什么?
    答:
    - 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
    - 一致性(Consistent):事务结束后系统状态是一致的;
    - 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
    - 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。

    补充:关于事务,在面试中被问到的概率是很高的,可以问的问题也是很多的。首先需要知道的是,只有存在并发数据访问时才需要事务。当多个事务访问同一数据时,可能会存在5类问题,包括3类数据读取问题(脏读、不可重复读和幻读)和2类数据更新问题(第1类丢失更新和第2类丢失更新)。

    脏读(Dirty Read):A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据。

    时间 转账事务A 取款事务B
    T1   开始事务
    T2 开始事务  
    T3   查询账户余额为1000元
    T4   取出500元余额修改为500元
    T5 查询账户余额为500元(脏读)  
    T6   撤销事务余额恢复为1000元
    T7 汇入100元把余额修改为600元  
    T8 提交事务  

    不可重复读(Unrepeatable Read):事务A重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务B修改过了。

    时间 转账事务A 取款事务B
    T1   开始事务
    T2 开始事务  
    T3   查询账户余额为1000元
    T4 查询账户余额为1000元  
    T5   取出100元修改余额为900元
    T6   提交事务
    T7 查询账户余额为900元(不可重复读)  

    幻读(Phantom Read):事务A重新执行一个查询,返回一系列符合查询条件的行,发现其中插入了被事务B提交的行。

    时间 统计金额事务A 转账事务B
    T1   开始事务
    T2 开始事务  
    T3 统计总存款为10000元  
    T4   新增一个存款账户存入100元
    T5   提交事务
    T6 再次统计总存款为10100元(幻读)  

    第1类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了。

    时间 取款事务A 转账事务B
    T1 开始事务  
    T2   开始事务
    T3 查询账户余额为1000元  
    T4   查询账户余额为1000元
    T5   汇入100元修改余额为1100元
    T6   提交事务
    T7 取出100元将余额修改为900元  
    T8 撤销事务  
    T9 余额恢复为1000元(丢失更新)  

    第2类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。

    时间 转账事务A 取款事务B
    T1   开始事务
    T2 开始事务  
    T3   查询账户余额为1000元
    T4 查询账户余额为1000元  
    T5   取出100元将余额修改为900元
    T6   提交事务
    T7 汇入100元将余额修改为1100元  
    T8 提交事务  
    T9 查询账户余额为1100元(丢失更新)  

    数据并发访问所产生的问题,在有些场景下可能是允许的,但是有些场景下可能就是致命的,数据库通常会通过锁机制来解决数据并发访问问题,按锁定对象不同可以分为表级锁和行级锁;按并发事务锁定关系可以分为共享锁和独占锁,具体的内容大家可以自行查阅资料进行了解。
    直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定会话的事务隔离级别,数据库就会通过分析SQL语句然后为事务访问的资源加上合适的锁,此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的(就是说你不用理解,事实上我确实也不知道)。ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,如下表所示:

    隔离级别 脏读 不可重复读 幻读 第一类丢失更新 第二类丢失更新
    READ UNCOMMITED 允许 允许 允许 不允许 允许
    READ COMMITTED 不允许 允许 允许 不允许 允许
    REPEATABLE READ 不允许 不允许 允许 不允许 不允许
    SERIALIZABLE 不允许 不允许 不允许 不允许 不允许

    需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方没有万能的原则。

    81、JDBC中如何进行事务处理?
    答:Connection提供了事务处理的方法,通过调用setAutoCommit(false)可以设置手动提交事务;当事务完成后用commit()显式提交事务;如果在事务处理过程中发生异常则通过rollback()进行事务回滚。除此之外,从JDBC 3.0中还引入了Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。
    这里写图片描述

    82、JDBC能否处理Blob和Clob?
    答: Blob是指二进制大对象(Binary Large Object),而Clob是指大字符对象(Character Large Objec),因此其中Blob是为存储大的二进制数据而设计的,而Clob是为存储大的文本数据而设计的。JDBC的PreparedStatement和ResultSet都提供了相应的方法来支持Blob和Clob操作。下面的代码展示了如何使用JDBC操作LOB:
    下面以MySQL数据库为例,创建一个张有三个字段的用户表,包括编号(id)、姓名(name)和照片(photo),建表语句如下:

    create table tb_user
    (
    id int primary key auto_increment,
    name varchar(20) unique not null,
    photo longblob
    );

    下面的Java代码向数据库中插入一条记录:

    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.PreparedStatement;
    import java.sql.SQLException;
    
    class JdbcLobTest {
    
        public static void main(String[] args) {
            Connection con = null;
            try {
                // 1. 加载驱动(Java6以上版本可以省略)
                Class.forName("com.mysql.jdbc.Driver");
                // 2. 建立连接
                con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
                // 3. 创建语句对象
                PreparedStatement ps = con.prepareStatement("insert into tb_user values (default, ?, ?)");
                ps.setString(1, "骆昊");              // 将SQL语句中第一个占位符换成字符串
                try (InputStream in = new FileInputStream("test.jpg")) {    // Java 7的TWR
                    ps.setBinaryStream(2, in);      // 将SQL语句中第二个占位符换成二进制流
                    // 4. 发出SQL语句获得受影响行数
                    System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失败");
                } catch(IOException e) {
                    System.out.println("读取照片失败!");
                }
            } catch (ClassNotFoundException | SQLException e) {     // Java 7的多异常捕获
                e.printStackTrace();
            } finally { // 释放外部资源的代码都应当放在finally中保证其能够得到执行
                try {
                    if(con != null && !con.isClosed()) {
                        con.close();    // 5. 释放数据库连接 
                        con = null;     // 指示垃圾回收器可以回收该对象
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    83、简述正则表达式及其用途。
    答:在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。

    说明:计算机诞生初期处理的信息几乎都是数值,但是时过境迁,今天我们使用计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符串匹配和处理的时候最为强大的工具,绝大多数语言都提供了对正则表达式的支持。

    84、Java中是如何支持正则表达式操作的?
    答:Java中的String类提供了支持正则表达式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java中可以用Pattern类表示正则表达式对象,它提供了丰富的API进行各种正则表达式操作,请参考下面面试题的代码。

    面试题: - 如果要从字符串中截取第一个英文左括号之前的字符串,例如:北京市(朝阳区)(西城区)(海淀区),截取结果为:北京市,那么正则表达式怎么写?

    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    class RegExpTest {
    
        public static void main(String[] args) {
            String str = "北京市(朝阳区)(西城区)(海淀区)";
            Pattern p = Pattern.compile(".*?(?=\\()");
            Matcher m = p.matcher(str);
            if(m.find()) {
                System.out.println(m.group());
            }
        }
    }

    说明:上面的正则表达式中使用了懒惰匹配和前瞻,如果不清楚这些内容,推荐读一下网上很有名的《正则表达式30分钟入门教程》

    85、获得一个类的类对象有哪些方式?
    答:
    - 方法1:类型.class,例如:String.class
    - 方法2:对象.getClass(),例如:"hello".getClass()
    - 方法3:Class.forName(),例如:Class.forName("java.lang.String")

    86、如何通过反射创建对象?
    答:
    - 方法1:通过类对象调用newInstance()方法,例如:String.class.newInstance()
    - 方法2:通过类对象的getConstructor()或getDeclaredConstructor()方法获得构造器(Constructor)对象并调用其newInstance()方法创建对象,例如:String.class.getConstructor(String.class).newInstance("Hello");

    87、如何通过反射获取和设置对象私有字段的值?
    答:可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。下面的代码实现了一个反射的工具类,其中的两个静态方法分别用于获取和设置私有字段的值,字段可以是基本类型也可以是对象类型且支持多级对象操作,例如ReflectionUtil.get(dog, "owner.car.engine.id");可以获得dog对象的主人的汽车的引擎的ID号。

    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.Modifier;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 反射工具类
     * @author 骆昊
     *
     */
    public class ReflectionUtil {
    
        private ReflectionUtil() {
            throw new AssertionError();
        }
    
        /**
         * 通过反射取对象指定字段(属性)的值
         * @param target 目标对象
         * @param fieldName 字段的名字
         * @throws 如果取不到对象指定字段的值则抛出异常
         * @return 字段的值
         */
        public static Object getValue(Object target, String fieldName) {
            Class<?> clazz = target.getClass();
            String[] fs = fieldName.split("\\.");
    
            try {
                for(int i = 0; i < fs.length - 1; i++) {
                    Field f = clazz.getDeclaredField(fs[i]);
                    f.setAccessible(true);
                    target = f.get(target);
                    clazz = target.getClass();
                }
    
                Field f = clazz.getDeclaredField(fs[fs.length - 1]);
                f.setAccessible(true);
                return f.get(target);
            }
            catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        /**
         * 通过反射给对象的指定字段赋值
         * @param target 目标对象
         * @param fieldName 字段的名称
         * @param value 值
         */
        public static void setValue(Object target, String fieldName, Object value) {
            Class<?> clazz = target.getClass();
            String[] fs = fieldName.split("\\.");
            try {
                for(int i = 0; i < fs.length - 1; i++) {
                    Field f = clazz.getDeclaredField(fs[i]);
                    f.setAccessible(true);
                    Object val = f.get(target);
                    if(val == null) {
                        Constructor<?> c = f.getType().getDeclaredConstructor();
                        c.setAccessible(true);
                        val = c.newInstance();
                        f.set(target, val);
                    }
                    target = val;
                    clazz = target.getClass();
                }
    
                Field f = clazz.getDeclaredField(fs[fs.length - 1]);
                f.setAccessible(true);
                f.set(target, value);
            }
            catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
    }

    88、如何通过反射调用对象的方法?
    答:请看下面的代码:

    import java.lang.reflect.Method;
    
    class MethodInvokeTest {
    
        public static void main(String[] args) throws Exception {
            String str = "hello";
            Method m = str.getClass().getMethod("toUpperCase");
            System.out.println(m.invoke(str));  // HELLO
        }
    }

    89、简述一下面向对象的"六原则一法则"。
    答:
    - 单一职责原则:一个类只做它该做的事情。(单一职责原则想表达的就是"高内聚",写代码最终极的原则只有六个字"高内聚、低耦合",就如同葵花宝典或辟邪剑谱的中心思想就八个字"欲练此功必先自宫",所谓的高内聚就是一个代码模块只完成一项功能,在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。我们都知道一句话叫"因为专注,所以专业",一个对象如果承担太多的职责,那么注定它什么都做不好。这个世界上任何好的东西都有两个特征,一个是功能单一,好的相机绝对不是电视购物里面卖的那种一个机器有一百多种功能的,它基本上只能照相;另一个是模块化,好的自行车是组装车,从减震叉、刹车到变速器,所有的部件都是可以拆卸和重新组装的,好的乒乓球拍也不是成品拍,一定是底板和胶皮可以拆分和自行组装的,一个好的软件系统,它里面的每个功能模块也应该是可以轻易的拿到其他系统中使用的,这样才能实现软件复用的目标。)
    - 开闭原则:软件实体应当对扩展开放,对修改关闭。(在理想的状态下,当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类就可以,不需要修改原来的任何一行代码。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱,如果不清楚如何封装可变性,可以参考《设计模式精解》一书中对桥梁模式的讲解的章节。)
    - 依赖倒转原则:面向接口编程。(该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代,请参考下面的里氏替换原则。)
    里氏替换原则:任何时候都可以用子类型替换掉父类型。(关于里氏替换原则的描述,Barbara Liskov女士的描述比这个要复杂得多,但简单的说就是能用父类型的地方就一定能使用子类型。里氏替换原则可以检查继承关系是否合理,如果一个继承关系违背了里氏替换原则,那么这个继承关系一定是错误的,需要对代码进行重构。例如让猫继承狗,或者狗继承猫,又或者让正方形继承长方形都是错误的继承关系,因为你很容易找到违反里氏替换原则的场景。需要注意的是:子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。)
    - 接口隔离原则:接口要小而专,绝不能大而全。(臃肿的接口是对接口的污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高度内聚的。例如,琴棋书画就应该分别设计为四个接口,而不应设计成一个接口中的四个方法,因为如果设计成一个接口中的四个方法,那么这个接口很难用,毕竟琴棋书画四样都精通的人还是少数,而如果设计成四个接口,会几项就实现几个接口,这样的话每个接口被复用的可能性是很高的。Java中的接口代表能力、代表约定、代表角色,能否正确的使用接口一定是编程水平高低的重要标识。)
    - 合成聚合复用原则:优先使用聚合或合成关系复用代码。(通过继承来复用代码是面向对象程序设计中被滥用得最多的东西,因为所有的教科书都无一例外的对继承进行了鼓吹从而误导了初学者,类与类之间简单的说有三种关系,Is-A关系、Has-A关系、Use-A关系,分别代表继承、关联和依赖。其中,关联关系根据其关联的强度又可以进一步划分为关联、聚合和合成,但说白了都是Has-A关系,合成聚合复用原则想表达的是优先考虑Has-A关系而不是Is-A关系复用代码,原因嘛可以自己从百度上找到一万个理由,需要说明的是,即使在Java的API中也有不少滥用继承的例子,例如Properties类继承了Hashtable类,Stack类继承了Vector类,这些继承明显就是错误的,更好的做法是在Properties类中放置一个Hashtable类型的成员并且将其键和值都设置为字符串来存储数据,而Stack类的设计也应该是在Stack类中放一个Vector对象来存储数据。记住:任何时候都不要继承工具类,工具是可以拥有并可以使用的,而不是拿来继承的。)
    - 迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。(迪米特法则简单的说就是如何做到"低耦合",门面模式和调停者模式就是对迪米特法则的践行。对于门面模式可以举一个简单的例子,你去一家公司洽谈业务,你不需要了解这个公司内部是如何运作的,你甚至可以对这个公司一无所知,去的时候只需要找到公司入口处的前台美女,告诉她们你要做什么,她们会找到合适的人跟你接洽,前台的美女就是公司这个系统的门面。再复杂的系统都可以为用户提供一个简单的门面,Java Web开发中作为前端控制器的Servlet或Filter不就是一个门面吗,浏览器对服务器的运作方式一无所知,但是通过前端控制器就能够根据你的请求得到相应的服务。调停者模式也可以举一个简单的例子来说明,例如一台计算机,CPU、内存、硬盘、显卡、声卡各种设备需要相互配合才能很好的工作,但是如果这些东西都直接连接到一起,计算机的布线将异常复杂,在这种情况下,主板作为一个调停者的身份出现,它将各个设备连接在一起而不需要每个设备之间直接交换数据,这样就减小了系统的耦合度和复杂度,如下图所示。迪米特法则用通俗的话来将就是不要和陌生人打交道,如果真的需要,找一个自己的朋友,让他替你和陌生人打交道。)

    这里写图片描述
    这里写图片描述

    90、简述一下你了解的设计模式。
    答:所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解其设计思路。
    在GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》中给出了三类(创建型[对类的实例化过程的抽象化]、结构型[描述如何将类或对象结合在一起形成更大的结构]、行为型[对在不同的对象之间划分责任和算法的抽象化])共23种设计模式,包括:Abstract Factory(抽象工厂模式),Builder(建造者模式),Factory Method(工厂方法模式),Prototype(原始模型模式),Singleton(单例模式);Facade(门面模式),Adapter(适配器模式),Bridge(桥梁模式),Composite(合成模式),Decorator(装饰模式),Flyweight(享元模式),Proxy(代理模式);Command(命令模式),Interpreter(解释器模式),Visitor(访问者模式),Iterator(迭代子模式),Mediator(调停者模式),Memento(备忘录模式),Observer(观察者模式),State(状态模式),Strategy(策略模式),Template Method(模板方法模式), Chain Of Responsibility(责任链模式)。
    面试被问到关于设计模式的知识时,可以拣最常用的作答,例如:
    - 工厂模式:工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。
    - 代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、保护代理、Cache代理、防火墙代理、同步化代理、智能引用代理。
    - 适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。
    - 模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。
    除此之外,还可以讲讲上面提到的门面模式、桥梁模式、单例模式、装潢模式(Collections工具类和I/O系统中都使用装潢模式)等,反正基本原则就是拣自己最熟悉的、用得最多的作答,以免言多必失。

    91、用Java写一个单例类。
    答:
    - 饿汉式单例

    public class Singleton {
        private Singleton(){}
        private static Singleton instance = new Singleton();
        public static Singleton getInstance(){
            return instance;
        }
    }
    • 懒汉式单例
    public class Singleton {
        private static Singleton instance = null;
        private Singleton() {}
        public static synchronized Singleton getInstance(){
            if (instance == null) instance = new Singleton();
            return instance;
        }
    }

    注意:实现一个单例有两点注意事项,①将构造器私有,不允许外界通过构造器创建对象;②通过公开的静态方法向外界返回类的唯一实例。这里有一个问题可以思考:Spring的IoC容器可以为普通的类创建单例,它是怎么做到的呢?

    92、什么是UML?
    答:UML是统一建模语言(Unified Modeling Language)的缩写,它发表于1997年,综合了当时已经存在的面向对象的建模语言、方法和过程,是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持。使用UML可以帮助沟通与交流,辅助应用设计和文档的生成,还能够阐释系统的结构和行为。

    93、UML中有哪些常用的图?
    答:UML定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结构,包括:用例图(use case diagram)、类图(class diagram)、时序图(sequence diagram)、协作图(collaboration diagram)、状态图(statechart diagram)、活动图(activity diagram)、构件图(component diagram)、部署图(deployment diagram)等。在这些图形化符号中,有三种图最为重要,分别是:用例图(用来捕获需求,描述系统的功能,通过该图可以迅速的了解系统的功能模块及其关系)、类图(描述类以及类与类之间的关系,通过该图可以快速了解系统)、时序图(描述执行特定任务时对象之间的交互关系以及执行顺序,通过该图可以了解对象能接收的消息也就是说对象能够向外界提供的服务)。
    用例图:
    这里写图片描述
    类图:
    这里写图片描述
    时序图:
    这里写图片描述

    94、用Java写一个冒泡排序。
    答:冒泡排序几乎是个程序员都写得出来,但是面试的时候如何写一个逼格高的冒泡排序却不是每个人都能做到,下面提供一个参考代码:

    import java.util.Comparator;
    
    /**
     * 排序器接口(策略模式: 将算法封装到具有共同接口的独立的类中使得它们可以相互替换)
     * @author骆昊
     *
     */
    public interface Sorter {
    
       /**
        * 排序
        * @param list 待排序的数组
        */
       public <T extends Comparable<T>> void sort(T[] list);
    
       /**
        * 排序
        * @param list 待排序的数组
        * @param comp 比较两个对象的比较器
        */
       public <T> void sort(T[] list, Comparator<T> comp);
    }
    import java.util.Comparator;
    
    /**
     * 冒泡排序
     * 
     * @author骆昊
     *
     */
    public class BubbleSorter implements Sorter {
    
        @Override
        public <T extends Comparable<T>> void sort(T[] list) {
            boolean swapped = true;
            for (int i = 1, len = list.length; i < len && swapped; ++i) {
                swapped = false;
                for (int j = 0; j < len - i; ++j) {
                    if (list[j].compareTo(list[j + 1]) > 0) {
                        T temp = list[j];
                        list[j] = list[j + 1];
                        list[j + 1] = temp;
                        swapped = true;
                    }
                }
            }
        }
    
        @Override
        public <T> void sort(T[] list, Comparator<T> comp) {
            boolean swapped = true;
            for (int i = 1, len = list.length; i < len && swapped; ++i) {
                swapped = false;
                for (int j = 0; j < len - i; ++j) {
                    if (comp.compare(list[j], list[j + 1]) > 0) {
                        T temp = list[j];
                        list[j] = list[j + 1];
                        list[j + 1] = temp;
                        swapped = true;
                    }
                }
            }
        }
    }

    95、用Java写一个折半查找。
    答:折半查找,也称二分查找、二分搜索,是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组已经为空,则表示找不到指定的元素。这种搜索算法每一次比较都使搜索范围缩小一半,其时间复杂度是O(logN)。

    import java.util.Comparator;
    
    public class MyUtil {
    
       public static <T extends Comparable<T>> int binarySearch(T[] x, T key) {
          return binarySearch(x, 0, x.length- 1, key);
       }
    
       // 使用循环实现的二分查找
       public static <T> int binarySearch(T[] x, T key, Comparator<T> comp) {
          int low = 0;
          int high = x.length - 1;
          while (low <= high) {
              int mid = (low + high) >>> 1;
              int cmp = comp.compare(x[mid], key);
              if (cmp < 0) {
                low= mid + 1;
              }
              else if (cmp > 0) {
                high= mid - 1;
              }
              else {
                return mid;
              }
          }
          return -1;
       }
    
       // 使用递归实现的二分查找
       private static<T extends Comparable<T>> int binarySearch(T[] x, int low, int high, T key) {
          if(low <= high) {
            int mid = low + ((high -low) >> 1);
            if(key.compareTo(x[mid])== 0) {
               return mid;
            }
            else if(key.compareTo(x[mid])< 0) {
               return binarySearch(x,low, mid - 1, key);
            }
            else {
               return binarySearch(x,mid + 1, high, key);
            }
          }
          return -1;
       }
    }

    说明:上面的代码中给出了折半查找的两个版本,一个用递归实现,一个用循环实现。需要注意的是计算中间位置时不应该使用(high+ low) / 2的方式,因为加法运算可能导致整数越界,这里应该使用以下三种方式之一:low + (high - low) / 2或low + (high – low) >> 1或(low + high) >>> 1(>>>是逻辑右移,是不带符号位的右移)

    展开全文
  • 常见Java面试知识点总结

    万次阅读 多人点赞 2018-03-30 16:23:26
    截止到2018年3月30日,牛客网提供了120道Java面试题,这里整理了部分较为重点的内容,而且对于答案有困惑,补充了解释内容,便于理解。全部Java题目可参考:https://www.nowcoder.com/ta/review-java 1. 什么是...

    截止到2018年3月30日,牛客网提供了120道Java面试题,这里整理了部分较为重点的内容,而且对于答案有困惑,补充了解释内容,便于理解。全部Java题目可参考:https://www.nowcoder.com/ta/review-java

    1. 什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

    • java虚拟机是执行字节码文件(.class)的虚拟机进程。。java源程序(.java)被编译器编译成字节码文件(.class)。然后字节码文件,将由java虚拟机,解释成机器码(不同平台的机器码不同)。利用机器码操作硬件和操作系统。
    • 因为不同的平台装有不同的JVM,它们能够将相同的.class文件,解释成不同平台所需要的机器码。正是因为有JVM的存在,java被称为平台无关的编程语言。

    2.JDK和JRE的区别是什么?

    • Java开发工具包(JDK)是完整的Java软件开发包,包含了JRE,编译器和其他的工具(比如:JavaDoc,Java调试器),可以让开发者开发、编译、执行Java应用程序。
    • Java运行时环境(JRE)。它包括Java虚拟机、Java核心类库和支持文件。它不包含开发工具(JDK)、编译器、调试器和其他工具。

    3.”static”关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?

    • “static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。
      Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。
    • java中也不可以覆盖private的方法,因为private修饰的变量和方法只能在当前类中使用,如果是其他的类继承当前类是不能访问到private变量或方法的,当然也不能覆盖。

    4.是否可以在static环境中访问非static变量?

    static变量在Java中是属于类的,它在所有的实例中的值是一样的。当类被Java虚拟机载入的时候,会对static变量进行初始化。如果你的代码尝试不用实例来访问非static的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。

    5.Java支持的数据类型有哪些?什么是自动拆装箱?

    • Java支持的数据类型包括两种:一种是基本数据类型,包含byte,char, short, boolean ,int, long, float, double;另一种是引用类型:如String等,其实是对象的引用,JVM中虚拟栈中存的是对象的地址,创建的对象实质在堆中,通过地址来找到堆中的对象的过程,即为引用类型。
    • 自动装箱就是Java编译器在基本数据类型和对应的对象包装类型间的转化,即int转化为Integer,自动拆箱是Integer调用其方法将其转化为int的过程。

    6.Java中的方法覆盖(Overriding)和方法重载(Overloading)是什么意思?

    • Java中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况。重载Override是一个类中多态性的一种表现。
    • 方法覆盖是说子类重新定义了父类的方法。方法覆盖必须有相同的方法名,参数列表和返回类型。覆盖者可能不会限制它所覆盖的方法的访问。在java中,子类可继承父类的方法,则不需要重新编写相同的方法。但有时子类并不想原封不动继承父类的方法,而是想做一定的修改,这就采用方法重写。方法重写又称方法覆盖。

    7.Java中,什么是构造方法?什么是构造方法重载?什么是复制构造方法?

    • 当新对象被创建的时候,构造方法会被调用。每一个类都有构造方法。在程序员没有给类提供构造方法的情况下,Java编译器会为这个类创建一个默认的构造方法。
    • Java中构造方法重载和方法重载很相似。可以为一个类创建多个构造方法。每一个构造方法必须有它自己唯一的参数列表。
    • Java不支持复制构造方法,如果你不自己写构造方法的情况下,Java不会创建默认的复制构造方法。

    8. Java支持多继承么?

    Java中类不支持多继承,只支持单继承(即一个类只有一个父类)。但是java中的接口支持多继承,,即一个子接口可以有多个父接口。(接口的作用是用来扩展对象的功能,一个子接口继承多个父接口,说明子接口扩展了多个功能,当类实现接口时,类就扩展了相应的功能)。

    9. 接口和抽象类的区别是什么?

    从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

    Java提供和支持创建抽象类和接口。它们的实现有共同点,不同点在于:

    • 接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法;
    • 类可以实现很多个接口,但是只能继承一个抽象类;
    • 类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的;
    • 抽象类可以在不提供接口方法实现的情况下实现接口;
    • Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量;
    • Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public;
    • 接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是,如果它包含main方法的话是可以被调用的。

    也可以参考JDK8中抽象类和接口的区别。

    10. 什么是值传递和引用传递?

    一般认为,java内的传递都是值传递. java中实例对象的传递是引用传递。

    • 值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量;
    • 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身。

    11. 进程与线程的区别?

    • 进程是执行着的应用程序,是程序的一种动态形式,是CPU、内存等资源占用的基本单位,而且进程之间相互独立,通信比较困难,进程在执行过程中,包含比较固定的入口,执行顺序,出口;
    • 线程是进程内部的一个执行序列,隶属于某个进程,一个进程可以有多个线程,线程不能占有CPU、内存等资源,而且线程之间共享一块内存区域,通信比较方便,线程的入口执行顺序这些过程被应用程序所控制。

    12. 创建线程有几种不同的方式?你喜欢哪一种?为什么?

    有四种方式可以用来创建线程。

    • 继承Thread类;
    • 实现Runnable接口;
    • 应用程序可以使用Executor框架来创建线程池;
    • 实现Callable接口。

    实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。

    13. 概括的解释下线程的几种可用状态。

    • 新建( new ):新创建了一个线程对象。
    • 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象的start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu的使用权。
    • 运行( running ):可运行状态( runnable )的线程获得了cpu时间片( timeslice ),执行程序代码。
    • 阻塞( block ):阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种:

    (一). 等待阻塞:运行( running )的线程执行 o.wait ()方法,JVM会把该线程放入等待队列( waitting queue )中。

    (二). 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。

    (三). 其他阻塞: 运行( running )的线程执行Thread.sleep ( long ms )或 t.join ()方法,或者发出了 I/O 请求时,JVM会把该线程置为阻塞状态。当 sleep ()状态超时、join()等待线程终止或者超时、或者I/O 处理完毕时,线程重新转入可运行( runnable )状态。

    • 死亡( dead ):线程run()、 main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
      image

    14. 同步方法和同步代码块的区别是什么?

    为何使用同步?
    java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(增删改查),将会导致数据的不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

    区别

    • 同步方法默认用this或者当前类class对象作为锁;
    • 同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;
    • 同步方法使用关键字 synchronized修饰方法,而同步代码块主要是修饰需要进行同步的代码,用synchronized(object){代码内容}进行修饰;

    15. 在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?

    监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次==只有一个线程执行同步代码块==。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。

    16.什么是死锁(deadlock)?

    所谓死锁是指多个进 程因==竞争资源==而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。死锁产生的4个必要条件:

    • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
    • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
    • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
    • 循环等待条件:存在一种进程资源的==循环等待链==,链中每一个进程已获得的资源同时被链中下一个进程所请求。

    17. 如何确保N个线程可以访问N个资源同时又不导致死锁?

    使用多线程的时候,一种非常简单的避免死锁的方式就是:==指定获取锁的顺序==,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

    18.Java集合类框架的基本接口有哪些?

    集合类接口指定了一组叫做元素的对象。集合类接口的每一种具体的实现类都可以选择以它自己的方式对元素进行保存和排序。有的集合类允许重复的键,有些不允许。
    Java集合类提供了一套设计良好的支持对一组对象进行操作的接口和类。Java集合类里面最基本的接口有:

    • Collection:代表一组对象,每一个对象都是它的子元素。
    • Set:不包含重复元素的Collection。
    • List:有顺序的collection,并且可以包含重复元素。
    • Map:可以把键(key)映射到值(value)的对象,键不能重复。

    19.为什么集合类没有实现Cloneable和Serializable接口?

    克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的具体实现来决定如何被克隆或者是序列化。

    20.什么是迭代器(Iterator)?

    Iterator接口提供了很多对集合元素进行迭代的方法。每一个集合类都包含了可以返回迭代器实例的
    迭代方法。迭代器可以在迭代的过程中删除底层集合的元素,但是不可以直接调用集合的
    remove(Object Obj)删除,可以通过迭代器的remove()方法删除。

    21.Iterator和ListIterator的区别是什么?

    下面列出了他们的区别:

    • Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
    • Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
    • ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。

    22.快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?

    Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。

    33.Java中的HashMap的工作原理是什么?

    Java中的HashMap是以键值对(key-value)的形式存储元素的。HashMap需要一个hash函数,它使用hashCode()和equals()方法来向集合/从集合添加和检索元素。当调用put()方法的时候,HashMap会计算key的hash值,然后把键值对存储在集合中合适的索引上。如果key已经存在了,value会被更新成新值。HashMap的一些重要的特性是它的容量(capacity),负载因子(load factor)和扩容极限(threshold resizing)

    34.hashCode()和equals()方法的重要性体现在什么地方?

    Java中的HashMap使用hashCode()和equals()方法来确定键值对的索引,当根据键获取值的时候也会用到这两个方法。如果没有正确的实现这两个方法,两个不同的键可能会有相同的hash值,因此,可能会被集合认为是相等的。而且,这两个方法也用来发现重复元素。所以这两个方法的实现对HashMap的精确性和正确性是至关重要的。

    35.HashMap和Hashtable有什么区别?

    HashMap和Hashtable都实现了Map接口,因此很多特性非常相似。但是,他们有以下不同点:

    • HashMap允许键和值是null,而Hashtable不允许键或者值是null。
    • Hashtable是同步的,而HashMap不是。因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境。
    • HashMap提供了可供应用迭代的键的集合,因此,HashMap是快速失败的。另一方面,Hashtable提供了对键的列举(Enumeration)。
    • 一般认为Hashtable是一个遗留的类。

    36. 数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用Array而不是ArrayList?

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

    37. ArrayList和LinkedList有什么区别?

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

    • ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以==元素列表==的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。
      相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。
    • LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

    38.Comparable和Comparator接口是干什么的?列出它们的区别。

    Java提供了只包含一个compareTo()方法的Comparable接口。这个方法可以个给两个对象排序。具体来说,它返回负数,0,正数来表明已经存在的对象小于,等于,大于输入对象。
    Java提供了包含compare()和equals()两个方法的Comparator接口。compare()方法用来给两个输入参数排序,返回负数,0,正数表明第一个参数是小于,等于,大于第二个参数。equals()方法需要一个对象作为参数,它用来决定输入参数是否和comparator相等。只有当输入参数也是一个comparator并且输入参数和当前comparator的排序结果是相同的时候,这个方法才返回true。


    接:https://www.nowcoder.com/questionTerminal/99f7d1f4f8374e419a6d6924d35d9530
    来源:牛客网
    Comparable & Comparator 都是用来实现集合中元素的比较、排序的,只是 Comparable是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序,所以,如想实现排序,就需要在集合外定义 Comparator 接口的方法或在集合内实现 Comparable 接口的方法。 Comparator位于包java.util下,而Comparable位于包 java.lang下 Comparable 是一个对象本身就已经支持自比较所需要实现的接口(如 String、Integer 自己就可以完成比较大小操作,已经实现了Comparable接口) 自定义的类要在加入list容器中后能够排序,可以实现Comparable接口,在用Collections类的sort方法排序时,如果不指定Comparator,那么就以自然顺序排序, 这里的自然顺序就是实现Comparable接口设定的排序方式。 而 Comparator 是一个专用的比较器,当这个对象不支持自比较或者自比较函数不能满足你的要求时,你可以写一个比较器来完成两个对象之间大小的比较。 可以说一个是自已完成比较,一个是外部程序实现比较的差别而已。 用 Comparator 是策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。 比如:你想对整数采用绝对值大小来排序,Integer 是不符合要求的,你不需要去修改 Integer 类(实际上你也不能这么做)去改变它的排序行为,只要使用一个实现了 Comparator 接口的对象来实现控制它的排序就行了。

    39.什么是Java优先级队列(Priority Queue)?

    PriorityQueue是一个基于优先级堆的无界队列,它的元素是按照自然顺序(natural order)排序的。在创建的时候,我们可以给它提供一个负责给元素排序的比较器。PriorityQueue不允许null值,因为他们没有自然顺序,或者说他们没有任何的相关联的比较器。最后,PriorityQueue不是线程安全的,入队和出队的时间复杂度是O(log(n))。

    40.你了解大O符号(big-O notation)么?你能给出不同数据结构的例子么?

    大O符号描述了当数据结构里面的元素增加的时候,算法的规模或者是一个渐进上界 。
    大O符号也可用来描述其他的行为,比如:内存消耗。因为集合类实际上是数据结构,我们一般使用大O符号基于时间,内存和性能来选择最好的实现。大O符号可以对大量数据的性能给出一个很好的说明。

    41.Enumeration接口和Iterator接口的区别有哪些?

    Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。

    42.HashSet和TreeSet有什么区别?

    • HashSet是由一个hash表来实现的,因此,它的元素是无序的。add(),remove(),contains()方法的时间复杂度是O(1)。
    • 另一方面,TreeSet是由一个树形的结构来实现的,它里面的元素是有序的。因此,add(),remove(),contains()方法的时间复杂度是O(logn)。

    43.Java中垃圾回收有什么目的?什么时候进行垃圾回收?

    垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行。
    垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。

    44.System.gc()和Runtime.gc()会做什么事情?

    这两个方法用来提示JVM要进行垃圾回收。但是,立即开始还是延迟进行垃圾回收是取决于JVM的。

    45.finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?

    垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法 但是在Java中很不幸,如果内存总是充足的,那么垃圾回收可能永远不会进行,也就是说filalize()可能永远不被执行,显然指望它做收尾工作是靠不住的。 那么finalize()究竟是做什么的呢?它最主要的用途是回收特殊渠道申请的内存。Java程序有垃圾回收器,所以一般情况下内存问题不用程序员操心。但有一种JNI(Java Native Interface)调用non-Java程序(C或C++),finalize()的工作就是回收这部分的内存。

    调用时机:当垃圾回收器要宣告一个对象死亡时,至少要经过两次标记过程:如果对象在进行可达性分析后发现没有和GC Roots相连接的引用链,就会被第一次标记,并且判断是否执行finalizer( )方法,如果对象覆盖finalizer( )方法且未被虚拟机调用过,那么这个对象会被放置在F-Queue队列中,并在稍后由一个虚拟机自动建立的低优先级的Finalizer线程区执行触发finalizer( )方法,但不承诺等待其运行结束。
    finalization的目的:对象逃脱死亡的最后一次机会。(只要重新与引用链上的任何一个对象建立关联即可。)但是不建议使用,运行代价高昂,不确定性大,且无法保证各个对象的调用顺序。可用try-finally或其他替代。

    46.如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?

    不会,在下一个垃圾回收周期中,这个对象将是可被回收的。

    不会立即释放对象占用的内存。 如果对象的引用被置为null,只是断开了当前线程栈帧中对该对象的引用关系,而 垃圾收集器是运行在后台的线程,只有当用户线程运行到安全点(safe point)或者安全区域才会扫描对象引用关系,扫描到对象没有被引用则会标记对象,这时候仍然不会立即释放该对象内存,因为有些对象是可恢复的(在 finalize方法中恢复引用 )。只有确定了对象无法恢复引用的时候才会清除对象内存。

    47.Java堆的结构是什么样子的?什么是堆中的永久代(Perm Gen space)?

    JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
    堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。

    永久代是用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类,永久代中一般包含:
    类的方法(字节码…)
    类名(Sring对象)
    .class文件读到的常量信息
    class对象相关的对象列表和类型列表 (e.g., 方法对象的array).
    JVM创建的内部对象
    JIT编译器优化用的信息

    48.串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?

    吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等规模和大规模数据的应用程序。而串行收集器对大多数的小应用(在现代处理器上需要大概100M左右的内存)就足够了。

    49.在Java中,对象什么时候可以被垃圾回收?

    当一个对象到GC Roots不可达时,在下一个垃圾回收周期中尝试回收该对象,如果该对象重写了finalize()方法,并在这个方法中成功自救(将自身赋予某个引用),那么这个对象不会被回收。但如果这个对象没有重写finalize()方法或者已经执行过这个方法,也自救失败,该对象将会被回收。

    50.JVM的永久代中会发生垃圾回收么?

    垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区
    (注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)

    51.Java中的两种异常类型是什么?他们有什么区别?

    Java中有两种异常:受检查的(checked)异常和不受检查的(unchecked)异常。不受检查的异常不需要在方法或者是构造函数上声明,就算方法或者是构造函数的执行可能会抛出这样的异常,并且不受检查的异常可以传播到方法或者是构造函数的外面。相反,受检查的异常必须要用throws语句在方法或者是构造函数上声明。这里有Java异常处理的一些小建议。

    接:https://www.nowcoder.com/questionTerminal/3ded1983c85c4ae197e005bd31777bc7
    来源:牛客网

    Throwable包含了错误(Error)和异常(Excetion两类)
    Exception又包含了运行时异常(RuntimeException, 又叫非检查异常)和非运行时异常(又叫检查异常)
    (1) Error是程序无法处理了, 如果OutOfMemoryError等, 这些异常发生时, java虚拟机一般会终止线程 .
    (2) 运行时异常都是RuntimeException类及其子类,如 NullPointerException、IndexOutOfBoundsException等, 这些异常是不检查的异常, 是在程序运行的时候可能会发生的, 所以程序可以捕捉, 也可以不捕捉. 这些错误一般是由程序的逻辑错误引起的, 程序应该从逻辑角度去尽量避免.
    (3) 检查异常是运行时异常以外的异常, 也是Exception及其子类, 这些异常从程序的角度来说是必须经过捕捉检查处理的, 否则不能通过编译. 如IOException、SQLException等。

    52.Java中Exception和Error有什么区别?

    Exception和Error都是Throwable的子类。Exception用于用户程序可以捕获的异常情况。Error定义了不期望被用户程序捕获的异常。

    53.throw和throws有什么区别?

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

    1、Throw用于方法内部,Throws用于方法声明上;
    2、Throw后跟异常对象,Throws后跟异常类型;
    3、Throw后只能跟一个异常对象,Throws后可以一次声明多种异常类型。

    54. 异常处理完成以后,Exception对象会发生什么变化?、

    Exception对象会在下一个垃圾回收过程中被回收掉。

    55. finally代码块和finalize()方法有什么区别?

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

    56. 什么是Applet?

    java applet是能够被包含在HTML页面中并且能被启用了java的客户端浏览器执行的程序。Applet主要用来创建动态交互的web应用程序。

    57. 解释一下Applet的生命周期。

    applet可以经历下面的状态:
    Init:每次被载入的时候都会被初始化。
    Start:开始执行applet。
    Stop:结束执行applet。
    Destroy:卸载applet之前,做最后的清理工作。

    58. 当applet被载入的时候会发生什么?

    首先,创建applet控制类的实例,然后初始化applet,最后开始运行。

    59. Applet和普通的Java应用程序有什么区别?

    applet是运行在启用了java的浏览器中,Java应用程序是可以在浏览器之外运行的独立的Java程序。但是,它们都需要有Java虚拟机。
    进一步来说,Java应用程序需要一个有特定方法签名的main函数来开始执行。Java applet不需要这样的函数来开始执行。
    最后,Java applet一般会使用很严格的安全策略,Java应用一般使用比较宽松的安全策略。

    60. Java applet有哪些限制条件?

    主要是由于安全的原因,给applet施加了以下的限制:
    applet不能够载入类库或者定义本地方法;不能在宿主机上读写文件;不能读取特定的系统属性;不能发起网络连接,除非是跟宿主机;不能够开启宿主机上其他任何的程序。

    61. 什么是不受信任的applet?

    不受信任的applet是不能访问或是执行本地系统文件的Java applet,默认情况下,所有下载的applet都是不受信任的。

    62. 从网络上加载的applet和从本地文件系统加载的applet有什么区别?

    当applet是从网络上加载的时候,applet是由applet类加载器载入的,它受applet安全管理器的限制。
    当applet是从客户端的本地磁盘载入的时候,applet是由文件系统加载器载入的。
    从文件系统载入的applet允许在客户端读文件,写文件,加载类库,并且也允许执行其他程序,但是,却通不过字节码校验。

    63. applet类加载器是什么?它会做哪些工作?

    当applet是从网络上加载的时候,它是由applet类加载器载入的。类加载器有自己的java名称空间等级结构。类加载器会保证来自文件系统的类有唯一的名称空间,来自网络资源的类有唯一的名称空间。
    当浏览器通过网络载入applet的时候,applet的类被放置于和applet的源相关联的私有的名称空间中。然后,那些被类加载器载入进来的类都是通过了验证器验证的。验证器会检查类文件格式是否遵守Java语言规范,确保不会出现堆栈溢出(stack overflow)或者下溢(underflow),传递给字节码指令的参数是正确的。

    64. applet安全管理器是什么?它会做哪些工作?

    applet安全管理器是给applet施加限制条件的一种机制。浏览器可以只有一个安全管理器。安全管理器在启动的时候被创建,之后不能被替换覆盖或者是扩展。

    65. 弹出式选择菜单(Choice)和列表(List)有什么区别?

    Choice是以一种紧凑的形式展示的,需要下拉才能看到所有的选项。Choice中一次只能选中一个选项。List同时可以有多个元素可见,支持选中一个或者多个元素。

    66. 什么是布局管理器?

    布局管理器用来在容器中组织组件。

    67. 滚动条(Scrollbar)和滚动面板(JScrollPane)有什么区别?

    Scrollbar是一个组件,不是容器。而ScrollPane是容器。ScrollPane自己处理滚动事件。

    68. 哪些Swing的方法是线程安全的?

    只有3个线程安全的方法: repaint(), revalidate(), and invalidate()。

    69. 说出三种支持重绘(painting)的组件。

    Canvas, Frame, Panel,和Applet支持重绘。

    70. 什么是JDBC?

    JDBC是允许用户在不同数据库之间做选择的一个抽象层。JDBC允许开发者用JAVA写数据库应用程序,而不需要关心底层特定数据库的细节。

    JDBC(Java DataBase Connectivity),是一套面向对象的应用程序接口(API),制定了统一的访问各类关系数据库的标准接口,为各个数据库厂商提供了标准的实现。通过JDBC技术,开发人员可以用纯Java语言和标准的SQL语句编写完整的数据库应用程序,并且真正地实现了软件的跨平台性。
    通常情况下使用JDBC完成以下操作:
    1.同数据库建立连接;
    2.向数据库发送SQL语句;
    3.处理从数据库返回的结果;
    JDBC具有下列优点:
    1.JDBC与ODBC(Open Database Connectivity,即开放数据库互连)十分相似,便于软件开发人员理解;
    2.JDBC使软件开发人员从复杂的驱动程序编写工作中解脱出来,可以完全专注于业务逻辑开发;
    3.JDBC支持多种关系型数据库,大大增加了软件的可移植性;
    4.JDBC API是面向对象的,软件开发人员可以将常用的方法进行二次封装,从而提高代码的重用性;

    71. 数据库连接池是什么意思?

    像打开关闭数据库连接这种和数据库的交互可能是很费时的,尤其是当客户端数量增加的时候,会消耗大量的资源,成本是非常高的。可以在应用服务器启动的时候建立很多个数据库连接并维护在一个池中。连接请求由池中的连接提供。在连接使用完毕以后,把连接归还到池中,以用于满足将来更多的请求。

    72. 让RMI程序能正确运行有哪些步骤?

    为了让RMI程序能正确运行必须要包含以下几个步骤:
    编译所有的源文件。
    使用rmic生成stub。
    启动rmiregistry。
    启动RMI服务器。
    运行客户端程序。

    73. 解释下Marshalling和demarshalling。

    当应用程序希望把内存对象跨网络传递到另一台主机或者是持久化到存储的时候,就必须要把对象在内存里面的表示转化成合适的格式。这个过程就叫做Marshalling,反之就是demarshalling。

    74. 解释下Serialization和Deserialization。

    Java提供了一种叫做对象序列化的机制,他把对象表示成一连串的字节,里面包含了对象的数据,对象的类型信息,对象内部的数据的类型信息等等。因此,序列化可以看成是为了把对象存储在磁盘上或者是从磁盘上读出来并重建对象而把对象扁平化的一种方式。反序列化是把对象从扁平状态转化成活动对象的相反的步骤。

    75. 什么是Servlet?

    Servlet是用来处理客户端请求并产生动态网页内容的Java类。Servlet主要是用来处理或者是存储HTML表单提交的数据,产生动态内容,在无状态的HTTP协议下管理状态信息。

    76. 说一下Servlet的体系结构。

    所有的Servlet都必须要实现的核心的接口是javax.servlet.Servlet。每一个Servlet都必须要直接或者是间接实现这个接口,或者是继承javax.servlet.GenericServlet或者javax.servlet.http.HTTPServlet。最后,Servlet使用多线程可以并行的为多个请求服务。

    77. 什么是Web应用程序?

    Web应用程序是对Web或者是应用服务器的动态扩展。有两种类型的Web应用:面向表现的和面向服务的。面向表现的Web应用程序会产生包含了很多种标记语言和动态内容的交互的web页面作为对请求的响应。而面向服务的Web应用实现了Web服务的端点(endpoint)。一般来说,一个Web应用可以看成是一组安装在服务器URL名称空间的特定子集下面的Servlet的集合。

    78. 如何知道是哪一个客户端的机器正在请求你的Servlet?

    ServletRequest类可以找出客户端机器的IP地址或者是主机名。getRemoteAddr()方法获取客户端主机的IP地址,getRemoteHost()可以获取主机名。

    79. HTTP响应的结构是怎么样的?

    HTTP响应由三个部分组成:
    状态码(Status Code):描述了响应的状态。可以用来检查是否成功的完成了请求。请求失败的情况下,状态码可用来找出失败的原因。如果Servlet没有返回状态码,默认会返回成功的状态码HttpServletResponse.SC_OK。
    HTTP头部(HTTP Header):它们包含了更多关于响应的信息。比如:头部可以指定认为响应过期的过期日期,或者是指定用来给用户安全的传输实体内容的编码格式。如何在Serlet中检索HTTP的头部看这里。
    主体(Body):它包含了响应的内容。它可以包含HTML代码,图片,等等。主体是由传输在HTTP消息中紧跟在头部后面的数据字节组成的。

    80. 什么是cookie?session和cookie有什么区别?

    cookie是Web服务器发送给浏览器的一块信息。浏览器会在本地文件中给每一个Web服务器存储cookie。以后浏览器在给特定的Web服务器发请求的时候,同时会发送所有为该服务器存储的cookie。下面列出了session和cookie的区别:
    无论客户端浏览器做怎么样的设置,session都应该能正常工作。客户端可以选择禁用cookie,但是,session仍然是能够工作的,因为客户端无法禁用服务端的session。
    在存储的数据量方面session和cookies也是不一样的。session能够存储任意的Java对象,cookie只能存储String类型的对象。

    81. JSP请求是如何被处理的?

    浏览器首先要请求一个以.jsp扩展名结尾的页面,发起JSP请求,然后,Web服务器读取这个请求,使用JSP编译器把JSP页面转化成一个Servlet类。需要注意的是,只有当第一次请求页面或者是JSP文件发生改变的时候JSP文件才会被编译,然后服务器调用servlet类,处理浏览器的请求。一旦请求执行结束,servlet会把响应发送给客户端。

    82. 封装的定义和好处有哪些?

    封装给对象提供了隐藏内部特性和行为的能力。对象提供一些能被其他对象访问的方法来改变它内部的数据。在Java当中,有3种修饰符:public,private和protected。每一种修饰符给其他的位于同一个包或者不同包下面对象赋予了不同的访问权限。
    下面列出了使用封装的一些好处:
    通过隐藏对象的属性来保护对象内部的状态。
    提高了代码的可用性和可维护性,因为对象的行为可以被单独的改变或者是扩展。
    禁止对象之间的不良交互提高模块化。
    参考这个文档获取更多关于封装的细节和示例。

    或:

    一是用private把类的细节与外界隔离起来,从而实现数据项和方法的隐藏,而要访问这些数据项和方法唯一的途径就是通过类本身,类才有资格调用它所拥有的资源(方法,数据项属性等等)。所以第一个好处就是数据的安全性提高了。
    二是通过隐藏隔离,只允许外部对类做有限的访问,开发者可以自由的改变类的内部实现,而无需修改使用该类的那些程序。只要那些在类外部就能被调用的方法保持其外部特征不变,内部代码就可以自由改变,各取所需,利于分工。
    三就是提高了代码的重用性,封装成工具类以后能够减少很多繁琐的步骤。

    83.抽象的定义?抽象和封装的不同点?

    • 抽象是把想法从具体的实例中分离出来的步骤,因此,要根据他们的功能而不是实现细节来创建类。Java支持创建只暴漏接口而不包含方法实现的抽象的类。这种抽象技术的主要目的是把类的行为和实现细节分离开。
    • 抽象和封装是互补的概念。一方面,抽象关注对象的行为。另一方面,封装关注对象行为的细节。一般是通过隐藏对象内部状态信息做到封装,因此,封装可以看成是用来提供抽象的一种策略。
    展开全文
  • 2019 java面试题基础

    万次阅读 多人点赞 2019-07-18 17:20:12
    如有需要获取最新资料,以及更全面的PDF版面试题,请关注微信公众号“西柚向西”,回复“面试题”... 最新最全的面试集合,请参考《最全最新java面试题系列全家桶(带答案)》 更多精彩,持续更新中。。。。 4、&a...
  • Java面试问题集锦(上篇)

    千次阅读 2018-08-27 23:44:35
    摘要:  本文对面试过程中经常会被问到的一些关于...考虑到篇幅太长,现将 《面试/笔试第五弹 —— Java面试问题集锦》 一文分为上下两篇:《面试/笔试第五弹 —— Java面试问题集锦(上篇)》和《面试/笔试第五...
  • Java 最常见的 200+ 面试题:面试必备

    万次阅读 多人点赞 2020-09-17 23:01:58
    这份面试清单是从我 2015 年做了 TeamLeader 之后开始收集的,一方面是给公司招聘用,另一方面是想用它来挖掘在 Java 技术栈中,还有那些知识点是我不知道的,我想找到这些技术盲点,然后修复它,以此来提高自己的...
  • java面试题大全(整理版)

    万次阅读 多人点赞 2018-03-05 19:12:11
    这几天在网上搜集各种java面试题:一是为了自己能复习方便,二是为了分享给大家~~题目都是来自网上大佬的分享,感谢大佬们的贡献~~(持续更新中...)1、面向对象的特征有哪些方面?- 抽象:抽象是将一类对象的共同...
  • Java面试笔试题大汇总一(最全+详细答案)

    万次阅读 多人点赞 2019-06-26 10:11:24
    本篇文章内容过多,只能分成两部分: 汇总一:... ... 2013年年底的时候,我看到了网上流传的一个叫做《Java面试题大全》的东西,认真的阅读了以后发现里面的很...
  • 最新Java面试题,常见面试题及答案汇总

    万次阅读 多人点赞 2020-07-27 17:29:03
    Java最新面试题、面试题答案汇总
  • Java基础知识面试题(2020最新版)

    万次阅读 多人点赞 2020-05-06 14:13:40
    文章目录Java概述何为编程什么是Javajdk1.5之后的三大版本JVM、JRE和JDK的关系什么是跨平台性?原理是什么Java语言有哪些特点什么是字节码?采用字节码的最大好处是什么什么是Java程序的主类?应用程序和小程序的...
  • 8年经验面试官详解 Java 面试秘诀

    万次阅读 多人点赞 2019-11-25 16:29:40
    作者 |胡书敏 责编 | 刘静 出品 | CSDN(ID:CSDNnews) ...在本文里,就将结合本人的面试经验,针对Java初学者、Java初级开发和Java开发,给出若干准备简历和准备面试的建议。 Java程序员准备和投递简历的实...
  • Java面试知识点汇总

    万次阅读 多人点赞 2020-06-26 15:21:23
    为了找到心仪的工作而努力吧 最近在刷各种面经,把学会的知识点汇总一下以便查漏补缺: 未完待续…
  • Java面试宝典

    万次阅读 多人点赞 2020-04-19 00:39:58
    Java 基础-------------------------------------------------------------------------------------------------- 1 基本数据类型----------------------------------------------------------------------------...
  • 挑战10个最难的Java面试题(附答案)【上】

    万次阅读 多人点赞 2019-11-28 14:25:27
    这是收集的10个最棘手的Java面试问题列表。这些问题主要来自 Java 核心部分 ,不涉及 Java EE 相关问题。你可能知道这些棘手的 Java 问题的答案,或者觉得这些不足以挑战你的 Java 知识,但这些问题都是容易在各种 ...
  • Java面试题内容聚合

    万次阅读 多人点赞 2019-12-04 14:08:10
    这是一份常见Java面试题分类汇总,希望对大家有用! 初级面试题 Java面试题-基础篇一 Java面试题-基础篇二 Java面试题-集合框架篇三 Java面试题-基础篇四 Java面试题-基础篇五 Java面试题-javaweb篇六 ...
  • Java面试题及答案2019版(上)

    万次阅读 多人点赞 2020-06-11 09:27:50
    1、面向对象的特征有哪些方面? 答:面向对象的特征主要有以下几个方面: 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些...
  • Java面试个人简历

    万次阅读 多人点赞 2019-04-24 11:46:07
    求职意向: Java开发工程师 专业技能: 熟练使用Java面向对象编程,具有良好的编程习惯以及CSDN技术文档编写习惯; 熟练使用Eclipse/IDEA等开发工具; 熟练SVN,Maven,Git等项目管理工具; 熟练Spring,SpringMVC,...
  • java面试之项目面试问题--不看面试会后悔的

    万次阅读 多人点赞 2020-07-08 15:36:11
    1.请描述下你做的项目:XXX系统是什么什么,主要目的是XXX,整个系统包含了哪些功能模块,然后说包含了哪些模块,自己做了哪些模块。2.结合项目谈谈你对MVC的理解MVC是model-view-controler的简称。...
1 2 3 4 5 ... 20
收藏数 371,875
精华内容 148,750
关键字:

java面试