zoukankan      html  css  js  c++  java
  • Java面试考点解析(1)-- 基础知识篇

    -------------------------------   一、Java核心语法  -------------------------------  

    1、JAVA的垮平台原理

      我们编写的Java源码,编译后会生成一种 .class 的字节码文件,再由JVM将字节码文件翻译成特定平台下的机器码然后运行。JVM也是一个软件,不同的平台有不同的版本,只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们编写的Java程序。在这个过程中,我们编写的Java程序没有做任何改变,仅仅是通过JVM这一”中间层“,就能在不同平台上运行,真正实现了“一次编译,到处运行”的目的。 

    2、JAVA面向对象的特征

      JAVA面向对象的特征包括“抽象、封装、继承和多态”。

    • 抽象:是指将现实中的一类实体的共同特性抽象出来,以类的形式进行定义;
    • 封装:封装是由类来实现的,我们将现实实体的属性和行为,通过类的成员属性和成员方法进行封装;
    • 继承:在Java语言中,类之间属于单继承关系,通过子类继承父类的方式,实现父类代码的重用;
    • 多态:通过子类对象向上转型为父类对象的方式,给父类对象引用不同的子类对象,从而表现出不同的行为。

    2-2、JDK 和 JRE 的区别是什么?

    • JDK(Java Development Kit):Java开发工具包,是Java开发环境的核心组件,提供了编译、调试和运行程序所需要的所有工具,可以让开发者开发、编译、运行Java应用程序。JDK包含了:JRE、编译器和其他的工具(如JavaDoc, Java调试器等)。
    • JRE(Java Runtime Environment):Java运行时环境,提供了运行Java程序的平台。它包括了Java虚拟机、Java核心类库和支持文件。
    • JVM :指Java虚拟机,当我们运行一个程序时,JVM负责将字节码转换为特定机器代码,JVM 提供了内存管理、垃圾回收和安全机制等。JVM独立于硬件和操作系统,正是 Java程序可以一次编写多处执行的原因。

      区别:

    • JDK 用于开发,JRE 用于运行 Java 程序;JDK 和 JRE 中都包含 JVM;
    • JVM 是 Java 编程语言的核心并且具有平台独立性。

    2-3、true、false 与 null 是关键字吗?goto 与 const 呢?

    • true、false 与 null 不是关键字。true、false 是布尔类型的字面常量,null 是引用类型的字面常量。
    • goto 与 const 是 Java 语言保留的关键字,即没有任何语法应用。
    • 所有的关键字都是小写的,要注意true,false,null, friendly,sizeof 都不是java的关键字,但也不能把它们作为java标识符用。

             参考资源:java中的关键字详解

    2-4、Java是值传递还是引用传递?

    • 一般认为,Java内的传递都是值传递。
    • Java中基本数据类型的传递采用值传递,传递的是该变量的一个副本,改变副本不影响原变量的值。
    • Java中实例对象的传递是引用传递(引用传递一般是对于对象型变量而言的),传递的是该对象地址的一个副本, 并不是原对象本身。但由于副本和原对象指向同一块内存空间,当改变副本的属性值时,原对象也会受到影响。

    3、重载(Overload)和重写(Override)的区别

      方法的重载和重写都是实现多态的方式,区别在于:重载实现的是编译时的多态性,重写实现的是运行时的多态性。

    • 重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重载对返回类型没有特殊的要求。
    • 重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。

    4、为什么不能根据返回类型来区分重载?

      重载是Java多态的一种体现,它实现的是编译时的多态,因为在调用时不能指定类型信息,编译器不知道你要调用哪个函数。 

              参考资源:为什么不能根据返回类型来区分重载? 

    4-1、“static” 关键字是什么意思?
      “static”关键字修饰的成员变量或成员方法,可以在没有所属的类的实例变量的情况下被访问。
      被static修饰的方法不能被子类重写,因为方法的重写是基于运行时动态绑定的,而静态方法是编译时静态绑定的,静态方法跟类的任何实例都不相关,所以不能被子类重写。

        扩展:private修饰的方法,也不能被重写。因为 private 修饰的方法只能在当前类中使用,如果是子类继承当前类是不能访问到 private 方法的,当然也不就不能被重写。

    4-2、i++ 与 ++i 到底有什么不同?
      对于这两个的区别,熟悉的表述是:前置++是先将变量的值加 1,然后使用加 1 后的值参与运算;而后置++则是先使用该值参与运算,然后再将该值加 1。但事实上,前置++和后置++一样,在参与运算之前都会将变量的值加 1,然后才继续计算的。
      二者之间真正的区别是:前置 ++ 是将变量的值加 1 后,使用增值后的变量进行运算的,而后置 ++ 是首先将变量赋值给一个临时变量,接下来对变量的值加 1,然后使用那个临时变量进行运算。

     5、什么是序列化和反序列化?

      序列化一般是指把结构化的对象变成无结构的字节流,以便于对象的存储和传输。

      JAVA序列化就是将对象按照某种协议格式(某种约定方式)放入一个字节流缓冲区中,其目的是便于持久存储和网络传输。

      JAVA反序列化,就是将序列化后的字节流缓冲区,还原成原来状态的对象,这样程序就能直接使用还原的对象了。

      注意:序列化并不是JAVA所独有的,基本所有编程语言都提供了序列化的方式,序列化是编程的一种解决问题的方式。

        要实现序列化,需要让一个类实现Serializable接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。
        序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆。
      在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。但是在真实的应用场景中,我们需要将这些对象持久化下来,并且能够在需要的时候把对象重新读取出来。Java的对象序列化可以帮助我们实现该功能。
      对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。
      在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。

             参考资源:Java对象的序列化与反序列化

     5-1、JAVA中实现对象序列化的方式?

      答:对象的序列化可以通过实现两种接口来实现:Serializable、Externalizable。

      若实现的是Serializable接口,则所有的序列化将会自动进行。

      若实现的是Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量(此时,transient修饰符不再起作用)。

    5-2、在对象序列化中,transient关键字的作用是什么?

      答:1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,即对象进行序列化时不会序列化该变量的当前值。

        2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

        3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

             参考资源:transient关键字使用小记

    6、两个对象值相同(x.equals(y) == true),但却可有不同的hashCode,这句话对不对? 

      不对,如果两个对象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》中是这样介绍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注解。

    7、String、StringBuilder、StringBuffer的区别

      (1)运行速度:StringBuilder >StringBuffer >String

       String为字符串常量,String对象一旦创建之后该对象是不可更改的,对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。而StringBuilder和StringBuffer均为字符串变量,是可以更改的,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。

      (2)线程安全:StringBuilder是线程不安全的,而StringBuffer是线程安全的

       StringBuffer中很多方法带有synchronized关键字,如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,可以保证线程是安全的;而StringBuilder的方法则没有该关键字,不能保证线程安全。

      (3)使用场景

    • String:适用于少量的字符串操作的情况
    • StringBuilder:适用于单线程下,在字符缓冲区进行大量操作的情况
    • StringBuffer:适用多线程下,在字符缓冲区进行大量操作的情况
      扩展一:String对象的intern方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与String对象的equals结果是true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;
      扩展二: 字符串的+操作其本质是创建了StringBuilder对象进行append操作,然后将拼接后的StringBuilder对象用toString方法处理成String对象,这一点可以用javap -c StringEqualTest.class命令获得class文件对应的JVM字节码指令就可以看出来。

    8、int和Integer有什么区别? 
      Java是一个近乎纯洁的面向对象编程语言,但是在早期的版本中引入了8种基本数据类型,为了能够将这些基本数据类型当成对象来操作,Java为每一个基本数据类型都定义了一个对应的包装类型(wrapper class),其中,Integer就是int的包装类。从jdk1.5开始引入了自动装箱/拆箱机制,使得基本数据类型和包装类型之间可以相互转换。 

      装箱的本质是什么呢?当我们给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,如果看看valueOf的源代码就知道发生了什么。简单的说,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象。

    9、 Math.floor() 和 Math.round() 的区别? 

    • Math.floor(double a) : 返回最大的(最接近正无穷大)double 值,该值小于或等于参数,并且等于某个整数。
    • Math.round(double a) :方法就是常说的四舍五入,原理是在参数上加0.5然后进行下取整【实际调用的是:Math.floor(a + 0.5) 】。比如,Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。

    10、char 型变量中能不能存贮一个中文汉字? 
      在Unicode编码集中,一个中文汉字占用两个字节;java语言中的char类型也是占用两个字节;由于Java虚拟机统一使用Unicode编码集,所以char可以存储一个中文汉字(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法)。

      补充:使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以Java中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader(继承Reader)和OutputStreamWriter(继承Writer),这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务。
    Unicode编码的起源:
      因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。最早的计算机在设计时采用8个比特(bit)作为一个字节(byte),所以,一个字节能表示的最大的整数就是255(二进制11111111=十进制255),0 - 255被用来表示大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码,比如大写字母A的编码是65,小写字母z的编码是122。
        如果要表示中文,显然一个字节是不够的,至少需要两个字节,而且还不能和ASCII编码冲突,所以,中国制定了GB2312编码,用来把中文编进去。类似的,日文和韩文等其他语言也有这个问题。为了统一所有文字的编码,Unicode应运而生。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。
        Unicode通常用两个字节表示一个字符,原有的英文编码从单字节变成双字节,只需要把高字节全部填为0就可以。

    11、Java中是如何支持正则表达式操作的? 

      Java中的String类提供了支持正则表达式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。

      另外,Java中可以用Pattern类表示正则表达式对象,它提供了丰富的API进行各种正则表达式操作。

    12、静态嵌套类(Static Nested Class)和内部类(Inner Class)有什么不同? 
      静态内部类,是指被static修饰的内部类,它可以不依赖于外部类的实例,而单独地被实例化。

      普通的内部类,需要在外部类实例化后,才能被实例化。

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

    -------------------------------   二、异常处理  -------------------------------  

    1、Error和Exception有什么区别? 

      Error表示系统级的错误和程序不必处理的异常,是恢复很困难的情况下的一种严重问题,比如虚拟机内存溢出:StackOverflowError、OutOfMemoryError。

      Exception表示需要被捕捉或者需要由程序进行处理的异常,是一种设计或实现问题,也就是说,如果程序运行正常,就不会发生的情况。

    StackOverflowError:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
    OutOfMemoryError:因为内存溢出或没有可用的内存提供给垃圾回收器时,Java 虚拟机无法分配一个对象,这时抛出该异常。
    提示:用递归编写程序时一定要牢记两点:1. 递归公式;2. 收敛条件(什么时候就不再继续递归)

    2、运行时异常与受检异常有何异同? 
      异常表示程序运行过程中可能出现的非正常状态,分为受检异常和运行时异常,定义如下:

    • 受检异常:编译时被强制检查的异常称为"受检异常",Java编译器要求方法必须声明抛出可能发生的受检异常。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发异常,比如:FileNotFoundException、ClassNotFoundException、IOException、DataFormatException
    • 运行时异常:方法定义时不需要声明可能发生的运行时异常。虚拟机在执行程序过程中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生该异常。常见的运行时异常有:NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException、ClassCastException。

      受检异常和运行时异常,二者的区别如下:

    • (1)Exception是所有异常的基类,RuntimeException是所有运行时异常的基类。
    • (2)受检异常要用try-catch块来处理,或者在方法定义时用throws关键字声明方法可能抛出哪些异常;运行时异常不要求被try-catch块处理或用throws语句声明。
    异常和继承一样,是面向对象程序设计中经常被滥用的东西,在Effective Java中对异常的使用给出了以下指导原则: 
    - 不要将异常处理用于正常的控制流(设计良好的API不应该强迫它的调用者为了正常的控制流而使用异常) 
    - 对可以恢复的情况使用受检异常,对编程错误使用运行时异常 
    - 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生) 
    - 优先使用标准的异常 
    - 每个方法抛出的异常都要有文档 
    - 保持异常的原子性 
    - 不要在catch中忽略掉捕获到的异常

               参考资源:Java受检异常和运行时异常解析

    3、Java语言如何进行异常处理的?关键字:throws、throw、try、catch、finally分别如何使用? 
      Java通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。

      在Java中,每个异常都是一个对象。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。

      Java的异常处理是通过5个关键词来实现的:try、catch、finally、throw和throws。

      一般情况下,用try来执行一段可能会产生异常的程序,如果程序抛出了一个异常对象,就用catch通过它的类型来捕获它。try用来指定一块预防所有异常的程序;catch子句紧跟在try块后面,用来指定你想要捕获的异常的类型;而finally是为确保不管是否发生异常,都要被执行的一段代码。

      throw语句,用在方法内部,用来明确地抛出一个异常;throws用在方法的声明上,用来声明一个方法可能抛出的各种异常(当然声明异常时允许无病呻吟)。

       try语句可以嵌套,每当遇到一个try语句,异常的结构就会被放入异常栈中,直到所有的try语句都完成。如果下一级的try语句没有对某种异常进行处理,异常栈就会执行出栈操作,直到遇到有处理这种异常的try语句或者最终将异常抛给JVM。

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

      如果存在finally代码块,try中的return语句不会立马返回调用者,而是记录下返回值,待finally代码块执行完毕之后再向调用者返回其值。如果在finally中修改了返回值,就会返回修改后的值。

        注意:在finally块中改变返回值的做法是不好的。在finally中返回或者修改返回值会对程序造成很大的困扰,Java中也可以通过提升编译器的语法检查级别来产生警告或错误。

     5、final、finally、finalize有什么区别?

    • final:修饰符关键字,可修饰类、方法和变量。修饰类时,该类不能被继承;修饰方法时,该方法在子类中不能被重写;修饰变量时,该变量必须在声明时给定初始值,之后只能读取不能被修改。
    • finally:在异常处理中使用,结构是try-catch-finally。在finally块中的代码,无论程序是否发生了异常,这里的代码都会被执行。
    • finalize:Object类中定义的方法,Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。
    protected void finalize()  throws Throwable
        当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写 finalize 方法,以配置系统资源或执行其他清除。
        finalize 的常规协定是:当 JavaTM 虚拟机已确定尚未终止的任何线程无法再通过任何方法访问此对象时,将调用此方法,除非由于准备终止的其他某个对象或类的终结操作执行了某个操作。finalize 方法可以采取任何操作,其中包括再次使此对象对其他线程可用;不过,finalize 的主要目的是在不可撤消地丢弃对象之前执行清除操作。例如,表示输入/输出连接的对象的 finalize 方法可执行显式 I/O 事务,以便在永久丢弃对象之前中断连接。
        Object 类的 finalize 方法执行非特殊性操作;它仅执行一些常规返回。Object 的子类可以重写此定义。
        Java 编程语言不保证哪个线程将调用某个给定对象的 finalize 方法。但可以保证在调用 finalize 时,调用 finalize 的线程将不会持有任何用户可见的同步锁定。如果 finalize 方法抛    出未捕获的异常,那么该异常将被忽略,并且该对象的终结操作将终止。
        在启用某个对象的 finalize 方法后,将不会执行进一步操作,直到 Java 虚拟机再次确定尚未终止的任何线程无法再通过任何方法访问此对象,其中包括由准备终止的其他对象或类执行的可能操作,在执行该操作时,对象可能被丢弃。
        对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法。
        finalize 方法抛出的任何异常都会导致此对象的终结操作停止,但可以通过其他方法忽略它。

    -------------------------------   三、IO流、NIO流  -------------------------------  

    1、Java中有几种类型的流? 
      有两种流,即:字节流和字符流。

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

    -------------------------------   四、集合  ------------------------------- 

    1、Collection和Collections的区别? 

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

    2、Array 和 ArrayList 有什么区别?

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

    3、如何权衡是使用无序的数组还是有序的数组?

    • 对于查询较多、插入较少的情况,建议使用有序数组;
    • 对于查询较少、插入较多的情况,建议使用无序数组;

      因为,有序数组最大的好处在于查找的时间复杂度是O(log n),而无序数组是O(n)。
      有序数组的缺点是插入操作的时间复杂度是O(n),因为插入时需要往后移动来给新元素腾位置。相反,无序数组的插入时间复杂度是常量O(1)。

    4、Java集合的快速失败机制 “fail-fast”
      “fail-fast”是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
      例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
      原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
      解决办法:

      (1)在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。

      (2)使用CopyOnWriteArrayList来替换ArrayList

    protected transient int modCount
        已从结构上修改 此列表的次数。从结构上修改是指更改列表的大小,或者以其他方式打乱列表,使正在进行的迭代产生错误的结果。
        此字段由 iterator 和 listIterator 方法返回的迭代器和列表迭代器实现来使用。如果意外更改了此字段中的值,则迭代器(或列表迭代器)将抛出 ConcurrentModificationException 来响应 next、remove、previous、set 或 add 操作。在迭代期间面临并发修改时,它提供了快速失败 行为,而不是非确定性行为。
    public class CopyOnWriteArrayList<E>
    extends Object
    implements List<E>, RandomAccess, Cloneable, Serializable
    ---------
    CopyOnWriteArrayList 是 ArrayList 的一个线程安全的变体,其中所有可变操作(添加、设置,等等)都是通过对基础数组进行一次新的复制来实现的。
        这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内绝不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。自创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。不支持迭代器上更改元素的操作(移除、设置和添加)。这些方法将抛出 UnsupportedOperationException。

    5、List、Map、Set三个接口存取元素时,各有什么特点?

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

    6、List、Set和Map的初始容量和加载因子分别是多少?
    1)List集合

    • ArrayList的初始容量是10;加载因子为0.5; 扩容增量:原容量的 0.5倍+1;第一次扩容后长度为15。
    • Vector初始容量为10,加载因子是1。扩容增量:原容量的 1倍,如 Vector的容量为10,一次扩容后是容量为20。

    2)Set集合

    • HashSet,初始容量为16,加载因子为0.75; 扩容增量:原容量的 1 倍; 如 HashSet的容量为16,第一次扩容后容量为32

    3)Map集合

    • HashMap,初始容量16,加载因子为0.75; 扩容增量:原容量的 1 倍; 如 HashMap的容量为16,第一次扩容后容量为32

            推荐阅读:ArrayList的扩容机制源码

            推荐阅读:HashMap的扩容机制源码 

    7、ARRAYLIST、Vector和LINKEDLIST的区别

      (1)相同点

    • ArrayList,Vector、LinkedList均实现了List接口,都可以通过索引来获取集合中的元素.
    • ArrayList和Vector都会在内存中开辟一块连续的空间来存储;
    • ArrayList和Vector都有一个初始化的容量大小,当存储的元素超过这个大小时就需要动态地扩充他们的存储空间。

      (2)不同点

    • Vector线程同步,ArrayList、LinkedList线程不同步。
    • Vector、ArrayList都是以类似数组的形式在内存中存储,因此适合查找,不适合指定位置的插入、删除操作。而LinkedList则以链表的形式进行存储,因此适合指定位置插入、删除操作,不适合查找。
    • Vector默认扩充为原来的两倍(每次扩充空间的大小是可以设置的),而ArratList默认扩充为原来的1.5倍,因此ArrayList更节省空间。
      Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,Vector属于遗留容器,已经不推荐使用,但是由于ArrayList和LinkedListed都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。
    -----------------------------------------------------------------------------------------
    public static <T> List<T> synchronizedList(List<T> list)

      返回由指定列表支持的同步(线程安全的)列表。为了保证按顺序访问,必须通过返回的列表完成对底层列表的所有访问。

      在返回的列表上进行迭代时,强制用户手工在返回的列表上进行同步:

      List list = Collections.synchronizedList(new ArrayList());

      synchronized(list) {

          Iterator i = list.iterator(); // Must be in synchronized block

          while (i.hasNext())

              foo(i.next());

      }

      Java早期的版本中提供的容器,Vector、Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器。遗留容器中的Properties类和Stack类在设计上有严重的问题,Properties是一个键和值都是字符串的特殊的键值对映射,在设计上应该是关联一个Hashtable并将其两个泛型参数设置为String类型,但是Java API中的Properties直接继承了Hashtable,这很明显是对继承的滥用。这里复用代码的方式应该是Has-A关系而不是Is-A关系,另一方面容器都属于工具类,继承工具类本身就是一个错误的做法,使用工具类最好的方式是Has-A关系(关联)或Use-A关系(依赖)。同理,Stack类继承Vector也是不正确的。

    8、如何去掉一个 Vector 集合中重复的元素?
    (1)循环遍历所有元素,对比后清除重复元素。
    (2)利用 Set 不允许重复元素的特性实现,将Vector转换为Set类型,从而自动去除重复的元素。

    //循环遍历所有元素,对比后清除重复元素;
    Vector newVector = new Vector(); for (int i = 0; i < vector.size(); i++) {   Object obj = vector.get(i);   if (!newVector.contains(obj)) {     newVector.add(obj);   } }
    //利用 Set 不允许重复元素的特性实现
    HashSet set = new HashSet(vector);

    9、TreeSet 和 TreeMap在排序时如何比较元素?

    • TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。
    • TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。

    9-1、Collections工具类中的sort()方法如何比较元素? 

      答:Collections类提供了两个sort()方法。

      第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;

      第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型【需要重写compare方法实现元素的比较:对于列表中的任何 e1 和 e2 元素,c.compare(e1, e2) 】,相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用

    public static <T extends Comparable<? super T>> void sort(List<T> list)  //第一种,元素e要实现Comparable接口
    public static <T> void sort(List<T> list, Comparator<? super T> c)     //第二种,c.compare(e1, e2)

    10、HASHMAP和HASHTABLE的区别

      (1)相同点:

    • HashMap和Hashtable都实现了Map接口,都可以用来存储key-value的数据

      (2)不同点:

    • 基类不同:HashTable的基类是Dictionary类,Dictionary类是任何可将键映射到值的类的抽象父类。而HashMap的基类是AbstractMap类,AbstractMap类是基于Map接口的骨干实现,它以最大限度地减少实现此接口所需的工作。
    • 遍历不同:Hashtable支持Iterator和Enumeration两种遍历方式,HashMap仅支持Iterator的遍历方式。
    • 对null的支持不同:HashTable中的key和value都不允许为null,而HashMap可以允许存在一个为null的key和任意个为null的value。
    • 线程安全:Hashtable是多线程安全的,而HashMap是多线程不安全的。

    11、ConcurrentHashMap 对 HashMap的增强作用有哪些?

      答:为了即能够保障线程安全又能够保证效率,在jdk1.5 中增加了ConcurrentHashMap类。

        ConcurrentHashMap类把整个Map分为N个segment(类似Hashtable),每个Segment各自持有一把锁,这样在保证线程安全的同时降低了锁的粒度,让并发操作效率更高(N个segment使效率提高N倍,默认是16倍)。

    12、ConcurrentHashMap 和 HashMap、Hashtable 的区别?

    • HashMap 没有考虑同步,多线程时是不安全的。
    • HashTable考虑了同步的问题。但是 HashTable在每次同步执行时都要锁住整个结构,执行效率比较低。 
    • ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势,采用更细粒度的锁来实现同步操作。ConcurrentHashMap 将 hash数组分为 16 个桶(默认初始化时长度为16),每个桶拥有单独的Segment锁,诸如执行get,put,remove 等操作时只锁当前需要用到的桶,从而提升执行效率。

    13、ConcurrentHashMap底层是如何实现的?
      答:ConcurrentHashMap类中包含两个静态内部类:HashEntry 和 Segment 。其中,HashEntry类用来封装映射表的键值对,Segment用来充当锁的角色。

        Segment 是一种可重入的锁 ReentrantLock每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

    14、HashMap 的长度为什么是2的幂次方?

      答:HashMap将 Key的hash值与 HashMap的长度(length - 1) 进行 位与 & 运算,来计算Key存储和查询的索引值。当HashMap的长度为 2 的幂次方时 ,length-1 转化为二进制必定是 11111…,此时进行位与运算,能保证key的索引分布均匀,即能够保证查询效率更快,而且保证内存空间不浪费。

     15、HashMap的底层实现你知道吗?

      在Java8之前,其底层实现是数组+链表实现,Java8使用了数组+链表+红黑树实现。

    16、jdk1.8中,HashMap中链表长度大于8时,会有怎样的变化?

      在JDK1.8及以后的版本中,HashMap引入了红黑树结构。若桶中链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。

      因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。 
      还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低

      使用HashMap时我们需要注意一下几点问题:
      1.HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现。与HashTable主要区别为不支持同步和允许null作为key和value。
      2.HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。
      3.如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
      4.在JDK1.6中,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。
      5.但是当位于一个数组中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
      6.JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值8时,将链表转换为红黑树,这样大大减少了查找时间。
      7.原本Map.Entry接口的实现类Entry改名为了Node。转化为红黑树时改用另一种实现TreeNode。

    -------------------------------   五、反射机制  ------------------------------- 

    1、获得一个类的class对象有哪些方式? 

    • 方法1:类.class,例如:String.class 
    • 方法2:对象.getClass(),例如:"hello".getClass() 
    • 方法3:Class.forName(),例如:Class.forName("java.lang.String")

    2、如何通过反射来创建对象? 

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

    3、如何通过反射获取和设置对象私有字段的值? 
      可以通过class对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。

    4、如何通过反射调用对象的方法? 

      通过class对象的getMethod()方法获取要调用的方法对象,然后使用方法对象的invoke(obj)方法来调用对象的方法,如:String.class.getMethod("toUpperCase").invoke("简小六");


    --------------------说明---------------------

    以上内容转载自以下资源:

    资源一:java开发工程师面试题总结

    资源二:Java面试题全集(上)

    资源三:Java面试题全集(中)

    资源四:Java面试题全集(下)

    资源五:Java 面试知识点解析(一)——基础知识篇

  • 相关阅读:
    Druid 使用 Kafka 将数据载入到 Kafka
    Druid 使用 Kafka 数据加载教程——下载和启动 Kafka
    Druid 集群方式部署 —— 启动服务
    Druid 集群方式部署 —— 端口调整
    Druid 集群方式部署 —— 配置调整
    Druid 集群方式部署 —— 配置 Zookeeper 连接
    Druid 集群方式部署 —— 元数据和深度存储
    Druid 集群方式部署 —— 从独立服务器部署上合并到集群的硬件配置
    Druid 集群方式部署 —— 选择硬件
    Druid 独立服务器方式部署文档
  • 原文地址:https://www.cnblogs.com/newbie27/p/10902684.html
Copyright © 2011-2022 走看看