文章目录有道无术,术尚可求,有术无道,止于术。
资料整理来自网络
- 1. JDK、JRE、JVM 有什么区别
- 2. == 和 equals 有什么区别
- 3. String类可以被继承吗?
- 4. String类为什么定义为final?
- 5. 重载和重写的区别
- 6. final关键字的用法
- 7. String、StringBuffer 和 StringBuilder 的区别
- 8. 接口和抽象类的区别
- 9. throw、throws的区别
- 10. & 和 && 的区别
- 11. break和continue的区别
- 12. 面向对象包括哪些特性
- 13. 访问权限修饰符 public、private、protected, 以及不写(默认)时的区别
- 14. Java中为什么要用 clone
- 15. new一个对象的过程和clone一个对象的区别
- 16. Java中实现多态的机制是什么?
- 17. 谈谈你对多态的理解?
- 18. final、finally、finalize 的区别?
- 19. Java中异常分为哪些种类?
- 20. error和exception的区别?
- 21. 调用下面的方法,得到的返回值是什么?
- 22. Java 异常处理机制的理解?
- 23. 说出最常见的5个RuntimeException?
- 24. switch是否能作用在byte、long、String上?
- 25. 数组有没有length()方法?String有没有length()方法?
- 26. Java 的基本数据类型都有哪些各占几个字节?
- 27. String 是基本数据类型吗?
- 28. short s1 = 1; s1 = s1 + 1; 有错吗?short s1 = 1; s1 += 1 有错吗?
- 29. int和Integer有什么区别?
- 30. 字符串如何转基本数据类型?
- 31. Java 中有几种类型的流?
- 32. 字节流如何转为字符流?
- 33. 字节流和字符流的区别?
- 34. 如何实现对象克隆?
- 35. 什么是 java 序列化,如何实现 java 序列化?
- 36. HashMap排序题
- 37. ArrayList、HashSet、HashMap 是线程安全的吗?如果不是怎么获取线程安全的集合?
- 38. ArrayList内部用什么实现的?
- 39. 并发集合和普通集合如何区别?
- 40. List 和 Map、Set 的区别?
- 41. HashMap和Hashtable有什么区别?
- 42. 数组和链表分别比较适合用于什么场景,为什么?
- 43. List a=new ArrayList()和ArrayList a =new ArrayList()的区别?
- 44 .请用两个队列模拟堆栈结构?
- 45. Map中的key和value可以为null?
- 46. HashSet 里的元素是不能重复的, 那用什么方法来区分重复与否呢?
- 47. List ,Set, Map是否继承来自Collection接口? 存取元素时, 有何差异?
- 48. BIO、NIO、AIO 有什么区别?
- 49. Java 中的值传递和引用传递?
- 50. static存在的主要意义
JDK 是Java Development Kit的缩写,即Java 开发工具包。JDK是用于制作程序和Java应用程序的软件开发环境。它包含了JRE和一些开发工具。
JRE的英文全称是 Java Runtime Environment,即Java运行时环境。它包含类库、加载器类和 JVM。
JVM 的英文全称是Java Virtual Machine。JVM 是一个引擎,它提供运行时环境驱动 Java 代码或应用程序。它将 Java 字节码转换为机器语言。
三者包含关系如下图:
对象类型不同:
- equals():是超类Object中的方法。
- ==:是操作符。
比较的对象不同:
-
equals():用来检测两个对象是否相等,即两个对象的内容是否相等。
-
==:用于比较引用和比较基本数据类型时具有不同的功能,具体如下:
-
基础数据类型:比较的是他们的值是否相等,比如两个int类型的变量,比较的是变量的值是否一样。
-
引用数据类型:比较的是引用的地址是否相同,比如说新建了两个User对象,比较的是两个User的地址是否一样。
-
运行速度不同:
-
equals():没有==运行速度快。
-
==:运行速度比equals()快,因为==只是比较引用。
如果类没有重写equals()方法,那么默认使用的就是 Object 类的方法,以下是 Object 类的 equals 方法:
// 里面使用的就是 == 比较,所以这种情况下比较的就是它们在内存中的存放地址。
public boolean equals(Object obj) {
return (this == obj);
}
String类复写了 equals 方法,当使用 == 比较内存的存放地址不相等时,接下来会比较字符串的内容是否相等,所以 String 类中的 equals 方法会比较两者的字符串内容是否一样。
对于以下代码,为啥返回False,会经常出现在面试题中
Integer aa = 128;
Integer bb = 128;
System.out.println(aa == bb); // false
这是因为Integer类内部通过静态内部类提供了一个缓存池,范围在-128~127之间,如果超过这个范围 Integer 值都是new出来的对象,使用==比较时,因为是引用类型,所以比较的是对象引用地址,也就是返回False了。
String类在声明时使用final关键字修饰,被final关键字修饰的类无法被继承。
String类的源代码片段:
public final class String
implements java.io.Serializable, Comparable,CharSequence {
private final char value[];
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
4. String类为什么定义为final?
因为只有当字符串是不可变的,字符串常量池才有可能实现。
字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。
如果字符串是可变的将会引发以下问题:
-
如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
-
那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
不可变的,带来以下好处:
-
因为不可变,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
-
因为不可变,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
重写(Override)是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。
重载(overloading) 是在一个类里面,而参数不同,返回类型可以相同也可以不同,名称相同的方法。
方法重载的规则:
-
方法名一致,参数列表中参数的顺序,类型,个数不同。
-
重载与方法的返回值无关,存在于父类和子类,同类中。
-
可以抛出不同的异常,可以有不同修饰符。
方法重写的规则:
-
参数列表、方法名、返回值类型必须完全一致;
-
构造方法不能被重写;
-
声明为 final 的方法不能被重写;
-
声明为 static 的方法不存在重写(重写和多态联合才有意义);
-
访问权限不能比父类更低;
-
重写之后的方法不能抛出更宽泛的异常;
final关键字可以用来修饰变量、方法和类。
-
修饰类:表示此类不能够被其他的类继承。例如String类、System类、StringBuffer类。
-
修饰方法:表示此方法不可以被重写。
-
修饰变量:表示此"变量"是一个常量。
- 修饰属性:可以在显示初始化、代码块中初始化、构造器中初始化。
- 修饰局部变量:修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参。一旦赋值以后,就只能在方法体内使用此形参,但不能进行修改重新赋值。
String:字符串常量,字符串长度不可变。用于存放字符的数组被声明为 final 的,因此只能赋值一次,不可再更改。
因此在每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,性能就会降低。
public final class String
implements java.io.Serializable, Comparable, CharSequence {
private final char value[];
private int hash; // Default to 0
}
String 对象赋值之后就会在字符串常量池中缓存,如果下次创建会判定常量池是否已经有缓存对象,如果有的话直接返回该引用给创建者。Java中的字符串常量池(String Pool)是Java堆内存中的一片内存空间。
StringBuilder 和 StringBuffer都继承于AbstractStringBuilder,底层使用的是没有用final修饰的字符数组char[]。所以在做字符串拼接的时候就在原来的内存上进行拼接,不会浪费内存空间。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
}
StringBuilder是线程不安全的,它的执行效率比StriingBuffer要高。
StringBuffer是线程安全的,它的执行效率比StringBuilder要低。
使用策略:
-
如果要操作少量的数据,用String ;单线程操作大量数据,用StringBuilder ;多线程操作大量数据,用StringBuffer。
-
不要使用String类的"+"来进行频繁的拼接,因为那样的性能极差的,应该使用StringBuffer或StringBuilder类,这在Java的优化上是一条比较重要的原则。
-
为了获得更好的性能,在构造 StringBuffer 或 StringBuilder 时应尽可能指定它们的容量。当然,如果你操作的字符串长度(length)不超过 16 个字符就不用了,当不指定容量(capacity)时默认构造一个容量为16的对象。不指定容量会显著降低性能。
-
StringBuilder 一般使用在方法内部来完成类似 + 功能,因为是线程不安全的,所以用完以后可以丢弃。StringBuffer 主要用在全局变量中。
-
相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。而在现实的模块化编程中,负责某一模块的程序员不一定能清晰地判断该模块是否会放入多线程的环境中运行,因此:除非确定系统的瓶颈是在 StringBuffer 上,并且确定你的模块不会运行在多线程模式下,才可以采用 StringBuilder;否则还是用 StringBuffer。
语法层面上的区别:
-
抽象类和接口都不能直接实例化。如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
-
接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
-
实现接口的关键字为 implements,继承抽象类的关键字为 extends。一个类可以实现多个接口,但一个类只能继承一个抽象类,因此使用接口可以间接地达到多重继承的目的。
-
接口只能做方法申明(JDK1.8以后可以添加默认方法),抽象类中可以做方法申明,也可以做方法实现。
-
抽象类可以有构造器,接口没有构造器。
-
抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
设计层面上的区别:
- 抽象类是对事物的抽象,即对类抽象;接口是对行为抽象,即局部抽象。抽象类对整体形为进行抽象,包括形为和属性。接口只对行为进行抽象。
- 抽象类是多个子类的像类,是一种模板式设计;接口是一咱形为规范,是一种辐射式设计。
throws是用来声明一个方法可能抛出的所有异常信息,throws是将异常声明但是不处理,而是将异常往上传,谁调用我就交给谁处理。而throw则是指抛出的一个具体的异常类型。
throw就是自己进行异常处理,处理的时候有两种方式,要么自己捕获异常(也就是try catch进行捕捉),要么声明抛出一个异常(就是throws 异常~~)。
throw一旦进入被执行,程序立即会转入异常处理阶段,后面的语句就不再执行,而且所在的方法不再返回有意义的值!
10. & 和 && 的区别&运算符是:逻辑与;&&运算符是:短路与。
&和&&在程序中最终的运算结果是完全一致的,只不过&&存在短路现象,当&&运算符左边的表达式结果为false的时候,右边的表达式不执行,此时就发生了短路现象。如果是&运算符,那么不管左边的表达式是true还是false,右边表达式是一定会执行的。这就是他们俩的本质区别。
&运算符还可以使用在二进制位运算上,例如按位与操作。
11. break和continue的区别break和continue 都是用来控制循环的语句。
break 用于完全结束一个循环,跳出循环体执行循环后面的语句。
continue 用于跳过本次循环,继续下次循环。
12. 面向对象包括哪些特性面向对象编程(OOP)其实就是一种设计思想,在程序设计过程中把每一部分都尽量当成一个对象来考虑,以实现软件系统的可扩展性,可维护性和可重用性。
封装(对应可扩展性):隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别。封装是通过访问控制符(public protected private)来实现。一个类就可看成一个封装。
继承(重用性和扩展性):子类继承父类,可以继承父类的方法和属性。可以对父类方向进行覆盖(实现了多态)。但是继承破坏了封装,因为他是对子类开放的,修改父类会导致所有子类的改变,因此继承一定程度上又破坏了系统的可扩展性,只有明确的IS-A关系才能使用。继承要慎用,尽量优先使用组合。
多态(可维护性和可扩展性):接口的不同实现方式即为多态。接口是对行为的抽象,刚才在封装提到,找到变化部分并封装起来,但是封装起来后,怎么适应接下来的变化?这正是接口的作用,接口的主要目的是为不相关的类提供通用的处理服务,我们可以想象一下。比如鸟会飞,但是超人也会飞,通过飞这个接口,我们可以让鸟和超人,都实现这个接口。
13. 访问权限修饰符 public、private、protected, 以及不写(默认)时的区别 14. Java中为什么要用 clone在实际编程过程中,我们常常要遇到这种情况:有一个对象 A,在某一时刻 A 中已经包含了一些有效值,此时可能会需要一个和 A 完全相同新对象 B,并且此后对 B 任何改动都不会影响到 A 中的值,也就是说,A 与 B 是两个独立的对象,但 B 的初始值是由 A 对象确定的。在 Java 语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但clone()方法是其中最简单,也是最高效的手段。
浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
15. new一个对象的过程和clone一个对象的区别new 操作符的本意是分配内存。程序执行到 new 操作符时,首先去看 new 操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
clone 在第一步是和 new 相似的,都是分配内存,调用 clone 方法时,分配的内存和原对象(即调用 clone 方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
16. Java中实现多态的机制是什么?Java中的多态靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法
17. 谈谈你对多态的理解?多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源代码,就可以让引用变量绑定到各种不同的对象上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
18. final、finally、finalize 的区别?final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承。
finally:异常处理语句结构的一部分,表示总是执行。
finalize:Object 类的一个方法,所以Java对象都有这个方法,当某Java对象没有更多的引用指向的时候,会被垃圾回收器回收,该对象被回收之前,由垃圾回收器来负责调用此方法,通常在该方法中进行回收前的准备工作。该方法更像是一个对象生命周期的临终方法,当该方法被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用。
19. Java中异常分为哪些种类?按照异常需要处理的时机分为编译时异常(也叫受控异常)也叫 CheckedException 和运行时异常(也叫非受控异常)也叫 UnCheckedException。Java认为Checked异常都是可以被处理的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked 异常,该程序在编译时就会发生错误无法编译。这体现了Java 的设计哲学:没有完善错误处理的代码根本没有机会被执行。对Checked异常处理方法有两种:
● 第一种:当前方法知道如何处理该异常,则用try…catch块来处理该异常。
● 第二种:当前方法不知道如何处理,则在定义该方法时声明抛出该异常。
运行时异常只有当代码在运行时才发行的异常,编译的时候不需要try…catch。Runtime如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。
20. error和exception的区别?Error类和Exception类的父类都是Throwable类,他们的区别如下:
● Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和预防,遇到这样的错误,建议让程序终止。
● Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
●Exception类又分为未检查异常(UnCheckedException)和受检查的异常(CheckedException)。运行时异常ArithmeticException,IllegalArgumentException编译能通过,但是一运行就终止了,程序不会处理运行时异常,出现这类异常,程序会终止。而受检查的异常,要么用 try…catch 捕获,要么用throws字句声明抛出,交给它的父类处理,否则编译不会通过。
21. 调用下面的方法,得到的返回值是什么?1. public int getNum() {
2. try {
3. int a = 1 / 0;
4. return 1;
5. } catch (Exception e) {
6. return 2;
7. } finally {
8. return 3;
9. }
10.}
代码走到第3行的时候遇到了一个MathException,这时第4行的代码就不会执行了,代码直接跳转到catch语句中,走到第 6 行的时候,异常机制有一个原则:如果在catch中遇到了return或者异常等能使该函数终止的话那么有finally就必须先执行完finally代码块里面的代码然后再返回值。因此代码又跳到第8行,可惜第8行是一个return语句,那么这个时候方法就结束了,因此第6行的返回结果就无法被真正返回。如果finally仅仅是处理了一个释放资源的操作,那么该道题最终返回的结果就是2。因此上面返回值是3。
22. Java 异常处理机制的理解?Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为 java.lang.Throwable,Throwable下面又派生了两个子类:Error和Exception。
Error表示应用程序本身无法克服和恢复的一种严重问题。
Exception表示程序还能够克服和恢复的问题,其中又分为系统异常和普通异常。
系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件死掉,例如,数组下标越界(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException)、类转换异常(ClassCastException)。
普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。
Java为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须try…catch处理或用throws声明继续抛给上层调用方法处理,所以普通异常也称为checked异常,而系统异常可以处理也可以不处理,所以编译器不强制用try…catch处理或用throws声明,所以系统异常也称为unchecked异常。
23. 说出最常见的5个RuntimeException?● java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。
●java.lang.ClassNotFoundException指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。
● java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。
●java.lang.IndexOutOfBoundsException数组角标越界异常,常见于操作数组对象时发生。
● java.lang.IllegalArgumentException 方法传递参数错误。
● java.lang.ClassCastException 数据类型转换异常。
●java.lang.NoClassDefFoundException未找到类定义错误。
● SQLException SQL 异常,常见于操作数据库时的 SQL 语句错误。
●java.lang.InstantiationException实例化异常。
●java.lang.NoSuchMethodException方法不存在异常。
24. switch是否能作用在byte、long、String上?Java5以前switch(expression)中,expression只能是byte、short、char、int,严格意义上来讲Java5以前只支持int,之所以能使用byte short char是因为存在自动类型转换。从 Java 5 开始,Java中引入了枚举类型,expression也可以是 enum 类型。从 Java 7 开始,expression还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
25. 数组有没有length()方法?String有没有length()方法?数组没有length()方法,而是有length属性。String有length()方法。JavaScript 中,获得字符串的长度是通过length属性得到的,这一点容易和Java混淆。
26. Java 的基本数据类型都有哪些各占几个字节?按照口诀记忆:
● 数据类型:byte short int long float double boolean char
● 占用字节数:12484812(byte对应1,short对应2,以此类推)
27. String 是基本数据类型吗?通过JDK源代码可以看到,Stirng是class,是引用类型,底层用 char 数组实现的。
28. 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);其中有隐含的强制类型转换。
29. 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
字符串如何转基本数据类型?
- 调用基本数据类型对应的包装类中的方法 parseXXX(String)或valueOf(String)即可返回相应基本类型。
基本数据类型如何转字符串?
- 一种方法是将基本数据类型与空字符串(“”)连接(+)即可获得其所对应的字符串;
- 另一种方法是调用 String类中的 valueOf()方法返回相应字符串。
按照流的方向:
- 输入流(inputStream)
- 输出流(outputStream)
按照实现功能分:
- 节点流(可以从或向一个特定的地方(节点)读写数据。如 FileReader)
- 处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如 BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。)
按照处理数据的单位:
- 字节流,字节流继承于 InputStream 和 OutputStream,
- 字符流,字符流继承于InputStreamReader 和 OutputStreamWriter 。
在 java 中能够被序列化的类必须先实现 Serializable 接口,该接口没有任何抽象方法只是起到一个标记作用。
public class Test {
public static void main(String[] args) throws Exception {
//对象输出流
ObjectOutputStream objectOutputStream =
new ObjectOutputStream(new FileOutputStream(new File("D://obj")));
objectOutputStream.writeObject(new User("zhangsan", 100));
objectOutputStream.close();
//对象输入流
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D://obj")));
User user = (User) objectInputStream.readObject();
System.out.println(user);
objectInputStream.close();
}
}
33. 字节流和字符流的区别?
读取方式:
- 字节流读取的时候,读到一个字节就返回一个字节;
- 字符流使用了字节流读到一个或多个字节(中文对应的字节数是两个,在 UTF-8 码表中是 3 个字节)时。先去查指定的编码表,将查到的字符返回。
处理类型:
- 字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件
- 字符流只能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。
- 字节流主要是操作 byte 类型数据,以 byte 数组为准,主要操作类就是 OutputStream、InputStream字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符、字符数组或字符串。
- 字节流处理单元为 1 个字节,操作字节和字节数组。所以字符流是由 Java 虚拟机将字节转化为 2 个字节的 Unicode 字符为单位的字符而成的,所以它对多国语言支持性比较好!如果是音频文件、图片、歌曲,就用字节流好点,如果是关系到中文(文本)的,用字符流好点。在程序中一个字符等于两个字节,java 提供了 Reader、Writer 两个专门操作字符流的类。
-
实现 Cloneable 接口并重写 Object 类中的 clone()方法;
-
实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下:
class MyUtil {
private MyUtil() {
throw new AssertionError();
}
public static 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 方法没有任何意义
// 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这不同于对外部资源(如文件流)的释放
}
}
**注意:**基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用 Object 类的 clone 方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。
35. 什么是 java 序列化,如何实现 java 序列化?序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。
序列化的实现 : 将需要被序列化类实现 Serializable 接 口 , 该 接 口 没 有 需 要 实 现 的 方 法 , implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream(对象流)对象,接着,使用 ObjectOutputStream 对象的 writeObject(Object obj)方法就可以将参数为 obj 的对象写出(即保存其状态),要恢复的话则用输入流。
36. HashMap排序题已知一个 HashMap
注意:要做出这道题必须对集合的体系结构非常的熟悉。HashMap本身就是不可排序的,但是该题偏偏让HashMap排序,那我们就得想在API中有没有这样的 Map 结构是有序的,我们不难发现其中LinkedHashMap就具有这样的结构,是链表结构有序的,更可喜的是他是 HashMap的子类,我们返回LinkedHashMap
但凡是对集合的操作,我们应该保持一个原则就是能用JDK中的API就用JDK中的 API,比如排序算法我们不应该去用冒泡或者选择,而是首先想到用 Collections 集合工具类。
import java.util.*;
class HashMapTest {
public static void main(String[] args) {
HashMap users = new HashMap<>();
users.put(1, new User("张三", 25));
users.put(3,new User("李四",22));
users.put(2, new User("王五", 28));
System.out.println(users);
HashMap sortHashMap = sortHashMap(users);
System.out.println(sortHashMap);
}
public static HashMapsortHashMap(HashMap map) { // 首先拿到 map 的键值对集合 Set > entrySet = map.entrySet(); // 将 set 集合转为 List 集合,为什么,为了使用工具类的排序方法 List > list = new ArrayList >(entrySet); // 使用 Collections 集合工具类对 list 进行排序,排序规则使用匿名内部类来实现 Collections.sort(list, new Comparator >() { @Override public int compare(Map.Entry o1, Map.Entry o2) { //按照要求根据 User 的 age 的倒序进行排 return o2.getValue().getAge() - o1.getValue().getAge(); } }); //创建一个新的有序的 HashMap 子类的集合 LinkedHashMap linkedHashMap = new LinkedHashMap (); //将 List 中的数据存储在 LinkedHashMap 中 for (Map.Entry entry : list) { linkedHashMap.put(entry.getKey(), entry.getValue()); } return linkedHashMap; } }
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
37. ArrayList、HashSet、HashMap 是线程安全的吗?如果不是怎么获取线程安全的集合?
通过以上类的源码进行分析,每个方法都没有加锁,显然都是非线程安全的。在集合中Vector 和HashTable是线程安全的。打开源码会发现其实就是把各自核心方法添加上了synchronized 关键字。Collections工具类提供了相关的 API,可以让上面那3个不安全的集合变为安全的。
- Collections.synchronizedCollection©;
- Collections.synchronizedList(list);
- Collections.synchronizedMap(m);
- Collections.synchronizedSet(s);
上面几个函数都有对应的返回值类型,传入什么类型返回什么类型。打开源码其实原理非常简单,就是将集合的核心方法添加上了synchronized关键字。
38. ArrayList内部用什么实现的?回答这样的问题,不要只回答个皮毛,可以再介绍一下ArrayList内部是如何实现数组的增加和删除的,因为数组在创建的时候长度是固定的,那么就有个问题我们往ArrayList中不断的添加对象,它是如何管理这些数组呢?通过源码可以看到ArrayList内部是用Object[]实现的。接下来我们分别分析ArrayList的构造以及add()、remove()、clear()方法的实现原理。
无参数构造方法:
public ArrayList(){
array=EmptyArray.OBJECT;
}
array 是一个 Object[]类型。当我们 new 一个空参构造时系统调用了 EmptyArray.OBJECT 属性,EmptyArray 仅仅是一个系统的类库,该类源码如下:
public final class EmptyArray {
private EmptyArray() {
}
public static final boolean[] BOOLEAN = new boolean[0];
public static final byte[] BYTE = new byte[0];
public static final char[] CHAR = new char[0];
public static final double[] DOUBLE = new double[0];
public static final int[] INT = new int[0];
public static final Class>[] CLASS = new Class[0];
public static final Object[] OBJECT = new Object[0];
public static final String[] STRING = new String[0];
public static final Throwable[] THROWABLE = new Throwable[0];
public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
}
也就是说当我们 new 一个空参 ArrayList 的时候,系统内部使用了一个 new Object[0]数组。
带容量参数的构造器:
public ArrayList(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity < 0: " + capacity);
}
array = (capacity == 0 ? EmptyArray.OBJECT : new Object[capacity]);
}
该构造函数传入一个 int 值,该值作为数组的长度值。如果该值小于 0,则抛出一个运行时异常。如果等于 0,则使用一个空数组,如果大于 0,则创建一个长度为该值的新数组。
带集合参数的构造器:
public ArrayList(Collection extends E> collection) {
if (collection == null) {
throw new NullPointerException("collection == null");
}
Object[] a = collection.toArray();
if (a.getClass() != Object[].class) {
Object[] newArray = new Object[a.length];
System.arraycopy(a, 0, newArray, 0, a.length);
a = newArray;
}
array = a;
size = a.length;
}
如果调用构造函数的时候传入了一个 Collection 的子类,那么先判断该集合是否为 null,为 null 则抛出空指针异常。如果不是则将该集合转换为数组 a,然后将该数组赋值为成员变量 array,将该数组的长度作为成员变量 size。
add方法:
@Override
public boolean add(E object) {
Object[] a = array;
int s = size;
if (s == a.length) {
Object[] newArray = new Object[s +
(s < (MIN_CAPACITY_INCREMENT / 2) ? MIN_CAPACITY_INCREMENT : s >> 1)];
System.arraycopy(a, 0, newArray, 0, s);
array = a = newArray;
}
a[s] = object;
size = s + 1;
modCount++;
return true;
}
-
首先将成员变量 array 赋值给局部变量 a,将成员变量 size 赋值给局部变量 s。
-
判断集合的长度 s 是否等于数组的长度(如果集合的长度已经等于数组的长度了,说明数组已经满了,该重新分 配 新 数 组 了 ) , 重 新 分 配 数 组 的 时 候 需 要 计 算 新 分 配 内 存 的 空 间 大 小 , 如 果 当 前 的 长 度 小 于MIN_CAPACITY_INCREMENT/2(这个常量值是 12,除以 2 就是 6,也就是如果当前集合长度小于 6)则分配 12 个长度,如果集合长度大于 6 则分配当前长度 s 的一半长度。这里面用到了三元运算符和位运算,s >> 1,意思就是将s 往右移 1 位,相当于 s=s/2,只不过位运算是效率最高的运算。
-
将新添加的 object 对象作为数组的 a[s]个元素。
-
修 改 集 合 长 度size为s+1。
-
modCount++,该变量是父类中声明的,用于记录集合修改的次数,记录集合修改的次数是为了防止在用迭代器迭代集合时避免并发修改异常,或者说用于判断是否出现并发修改异常的。
-
return true,这个返回值意义不大,因为一直返回 true,除非报了一个运行时异常。
remove方法:
@Override
public E remove(int index) {
Object[] a = array;
int s = size;
if (index >= s) {
throwIndexOutOfBoundsException(index, s);
}
@SuppressWarnings("unchecked") E result = (E) a[index];
System.arraycopy(a, index + 1, a, index, --s - index);
a[s] = null; // Prevent memory leak
size = s;
modCount++;
return result;
}
- 先将成员变量 array 和 size 赋值给局部变量 a 和 s。
- 判断形参 index 是否大于等于集合的长度,如果成了则抛出运行时异常
- 获取数组中脚标为 index 的对象 result,该对象作为方法的返回值
- 调用 System 的 arraycopy 函数完成数组拷贝。
- 接下来就是很重要的一个工作,因为删除了一个元素,而且集合整体向前移动了一位,因此需要将集合最后一个元素设置为 null,否则就可能内存泄露。
- 重新给成员变量 array 和 size 赋值。
- 记录修改次数。
- 返回删除的元素。
clear方法:
@Override
public void clear() {
if (size != 0) {
Arrays.fill(array, 0, size, null);
size = 0;
modCount++;
}
}
如果集合长度不等于 0,则将所有数组的值都设置为 null,然后将成员变量 size 设置为 0 即可,最后让修改记录加 1。
39. 并发集合和普通集合如何区别?并发集合常见的有ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque等。并发集合位于java.util.concurrent包下,是jdk1.5之后才有的,在 java 中有普通集合、同步(线程安全)的集合、并发集合。
普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。线程安全集合仅仅是给集合添加了 synchronized 同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率。
40. List 和 Map、Set 的区别?结构特点:
- List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合
- List 中存储的数据是有顺序,并且允许重复;
- Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的
- Set 中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的 hashCode 决定,位置是固定的(Set 集合根据 hashCode 来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 Set 中的元素还是无序的);
List 接口下的实现类:
- LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢;
- ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便于插入删除;
- Vector:基于数组实现,线程安全的,效率低。
Map 接口下的实现类:
- HashMap:基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键;
- Hashtable:线程安全,低效,不支持 null 值和 null 键;
- LinkedHashMap:是HashMap 的一个子类,保存了记录的插入顺序;
- TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序。
Set 接口下的实现类:
- HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hashCode()方法;
- LinkedHashSet继承与 HashSet,同时又基于LinkedHashMap 来进行实现,底层使用的是LinkedHashMp。
区别:
- List集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过list.get(i)方法来获取集合中的元素;
- Map中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;
- Set集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如 TreeSet类,可以按照默认顺序,也可以通过实现 java.util.Comparator接口来自定义排序方式。
HashMap是非线程安全的,HashMap是Map的一个实现类,是将键映射到值的对象,不允许键值重复。允许空键和空值;由于非线程安全,HashMap的效率要较 Hashtable 的效率高一些。
Hashtable 是线程安全的一个集合,不允许 null 值作为一个 key 值或者value 值;Hashtable是sychronized,多个线程访问时不需要自己为它的方法实现同步,而 HashMap 在被多个线程访问的时候需要自己为它的方法实现同步。
42. 数组和链表分别比较适合用于什么场景,为什么?ArrayList和Vector使用了数组的实现,可以认为 ArrayList 或者 Vector 封装了对内部数组的操作,比如向数组中添加、删除、插入新的元素或者数据的扩展和重定向。
LinkedList 使用了循环双向链表数据结构。与基于数组的 ArrayList 相比,这是两种截然不同的实现技术,这也决定了它们将适用于完全不同的工作场景。
LinkedList 链表由一系列表项连接而成。一个表项总是包含 3 个部分:元素内容,前驱表和后驱表,如图所示:
在下图展示了一个包含 3 个元素的 LinkedList 的各个表项间的连接关系。在 JDK 的实现中,无论 LikedList 是否为空,链表内部都有一个 header 表项,它既表示链表的开始,也表示链表的结尾。表项 header 的后驱表项便是链表中第一个元素,表项 header 的前驱表项便是链表中最后一个元素。
ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。如果集合数据是对于集合随机访问 get 和 set,ArrayList 绝对优于 LinkedList,因为 LinkedList 要移动指针。如果集合数据是对于集合新增和删除操作 add 和 remove,LinkedList 比较占优势,因为ArrayList要移动数据。
ArrayList 和 LinkedList 是两个集合类,用于存储一系列的对象引用(references)。例如我们可以用 ArrayList 来存储一系列的 String 或者 Integer。那么 ArrayList 和 LinkedList 在性能上有什么差别呢?什么时候应该用 ArrayList 什么时候又该用 LinkedList 呢?
● 时间复杂度
首先一点关键的是,ArrayList 的内部实现是基于基础的对象数组的,因此,它使用 get 方法访问列表中的任意一个元素时(random access),它的速度要比 LinkedList 快。LinkedList 中的 get 方法是按照顺序从列表的一端开始检查,直到另外一端。对 LinkedList 而言,访问列表中的某个指定元素没有更快的方法了。
假设我们有一个很大的列表,它里面的元素已经排好序了,这个列表可能是 ArrayList 类型的也可能是 LinkedList 类型的,现在我们对这个列表来进行二分查找(binary search),比较列表是 ArrayList 和 LinkedList 时的查询速度,看下面的程序:
public class TestList {
public static final int N = 50000; //50000 个数
public static List values; //要查找的集合
//放入 50000 个数给 value;
static {
Integer vals[] = new Integer[N];
Random r = new Random();
for (int i = 0, currval = 0; i < N; i++) {
vals = new Integer(currval);
currval += r.nextInt(100) + 1;
}
values = Arrays.asList(vals);
}
//通过二分查找法查找
static long timeList(List lst) {
long start = System.currentTimeMillis();
for (int i = 0; i < N; i++) {
int index = Collections.binarySearch(lst, values.get(i));
if (index != i)
System.out.println("***错误***");
}
return System.currentTimeMillis() - start;
}
public static void main(String args[]) {
System.out.println("ArrayList 消耗时间:" + timeList(new ArrayList(values)));
System.out.println("LinkedList 消耗时间:" + timeList(new LinkedList(values)));
}
}
LinkedList 做随机访问所消耗的时间与这个 list 的大小是成比例的。而相应的,在 ArrayList 中进行随机访问所消耗的时间是固定的。
这是否表明 ArrayList 总是比 LinkedList 性能要好呢?这并不一定,在某些情况下 LinkedList 的表现要优于ArrayList,有些算法在 LinkedList 中实现时效率更高。比方说,利用 Collections.reverse 方法对列表进行反转时,其性能就要好些。看这样一个例子,加入我们有一个列表,要对其进行大量的插入和删除操作,在这种情况下LinkedList 就是一个较好的选择。请看如下一个极端的例子,我们重复的在一个列表的开端插入一个元素:
public class ListDemo {
static final int N = 50000;
static long timeList(List list) {
long start = System.currentTimeMillis();
Object o = new Object();
for (int i = 0; i < N; i++)
list.add(0, o);
return System.currentTimeMillis() - start;
}
public static void main(String[] args) {
System.out.println("ArrayList 耗时:" + timeList(new ArrayList()));
System.out.println("LinkedList 耗时:" + timeList(new LinkedList()));
}
}
输出结果是:
ArrayList 耗时:2463 LinkedList 耗时:15
● 空间复杂度
在 LinkedList 中有一个私有的内部类,定义如下:
private static class Entry {
Object element;
Entry next;
Entry previous;
}
每个 Entry对象reference列表中的一个元素,同时还有在 LinkedList 中它的上一个元素和下一个元素。一个有1000个元素的LinkedList 对象将有 1000 个链接在一起的 Entry 对象,每个对象都对应于列表中的一个元素。这样的话,在一个 LinkedList 结构中将有一个很大的空间开销,因为它要存储这 1000 个 Entity 对象的相关信息。
ArrayList 使用一个内置的数组来存储元素,这个数组的起始容量是10。当数组需要增长时,新的容量按如下公式获得:新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长 50%。这就意味着,如果你有一个包含大量元素的 ArrayList 对象,那么最终将有很大的空间会被浪费掉,这个浪费是由ArrayList 的工作方式本身造成的。如果没有足够的空间来存放新的元素,数组将不得不被重新进行分配以便能够增加新的元素。对数组进行重新分配,将会导致性能急剧下降。如果我们知道一个ArrayList将会有多少个元素,我们可以通过构造方法来指定容量。我们还可以通过 trimToSize 方法在 ArrayList 分配完毕之后去掉浪费掉的空间。
● 总结
ArrayList 和 LinkedList 在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下:
- 对 ArrayList 和 LinkedList 而言,在列表末尾增加一个元素所花的开销都是固定的。对 ArrayList 而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对 LinkedList 而言,这个开销是统一的,分配一个内部 Entry 对象。
- 在 ArrayList 的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在 LinkedList 的中间插入或删除一个元素的开销是固定的。
- LinkedList 不支持高效的随机元素访问。
- ArrayList 的空间浪费主要体现在在 list 列表的结尾预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗相当的空间。
当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList 会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
43. List a=new ArrayList()和ArrayList a =new ArrayList()的区别?List list = new ArrayList();这句创建了一个 ArrayList 的对象后赋给了List。此时它是一个 List 对象了,有些ArrayList 有但是 List 没有的属性和方法,它就不能再用了。而ArrayList list=new ArrayList();创建一对象则保留了ArrayList 的所有属性。 所以需要用到 ArrayList 独有的方法的时候不能用前者。
实例代码如下:
List list = new ArrayList(); ArrayList arrayList = new ArrayList(); list.trimToSize(); //错误,没有该方法。 arrayList.trimToSize(); //ArrayList 里有该方法。44 .请用两个队列模拟堆栈结构?
两个队列模拟一个堆栈,队列是先进先出,而堆栈是先进后出。模拟如下队列 a 和 b:
● 入栈:a 队列为空,b 为空。例:则将”a,b,c,d,e”需要入栈的元素先放 a 中,a 进栈为”a,b,c,d,e”出栈:a 队列目前的元素为”a,b,c,d,e”。将 a 队列依次加入 Arraylist 集合 a 中。以倒序的方法,将 a 中的集合取出,放入 b 队列中,再将 b 队列出列。代码如下:
public static void main(String[] args) {
Queue queue = new LinkedList(); //a 队 列
Queue queue2 = new LinkedList(); //b 队列
ArrayList a = new ArrayList(); //arrylist 集合是中间参数
//往 a 队列添加元素
queue.offer("a");
queue.offer("b");
queue.offer("c");
queue.offer("d");
queue.offer("e");
System.out.print("进栈:"); //a 队列依次加入 list 集合之中
for (String q : queue) {
a.add(q);
System.out.print(q);
}
//以倒序的方法取出(a 队列依次加入 list 集合)之中的值,加入 b 对列
for (int i = a.size() - 1; i >= 0; i--) {
queue2.offer(a.get(i));
}
//打印出栈队列
System.out.println("");
System.out.print("出栈:");
for (String q : queue2) {
System.out.print(q);
}
}
运行结果为(遵循栈模式先进后出):
进栈:a b c d e 出栈:e d c b a45. Map中的key和value可以为null?
HashMap 对象的 key、value 值均可为 null。Hahtable 对象的 key、value 值均不可为 null。且两者的的 key 值均不能重复,若添加 key 相同的键值对,后面的 value 会自动覆盖前面的 value,但不会报错。测试代码如下:
public class Test {
public static void main(String[] args) {
Map map = new HashMap();//HashMap 对象
Map tableMap = new Hashtable();//Hashtable 对象
map.put(null, null);
System.out.println("hashMap 的[key]和[value]均可以为 null:" + map.get(null));
try {
tableMap.put(null, "3");
System.out.println(tableMap.get(null));
} catch (Exception e) {
System.out.println("【ERROR】:hashtable 的[key]不能为 null");
}
try {
tableMap.put("3", null);
System.out.println(tableMap.get("3"));
} catch (Exception e) {
System.out.println("【ERROR】:hashtable的[value]不能为null");
}
}
}
运行结果:
hashMap 的[key]和[value]均可以为 null:null 【ERROR】:hashtable 的[key]不能为 null 【ERROR】:hashtable 的[value]不能为 null46. HashSet 里的元素是不能重复的, 那用什么方法来区分重复与否呢?
往集合在添加元素时,调用 add(Object)方法的时候,首先会调用Object的 hashCode()方法判断hashCode 是否已经存在,如不存在则直接插入元素;如果已存在则调用Object对象的 equals()方法判断是否返回 true,如果为true则说明元素已经存在,如为false则插入元素。
47. List ,Set, Map是否继承来自Collection接口? 存取元素时, 有何差异?List、Set 是继承 Collection 接口; Map不是。
List:元素有放入顺序,元素可重复,通过下标来存取。
Map:元素按键值对存取,无放入顺序。
Set:元素无存取顺序,元素不可重复(注意:元素虽然无放入顺序,但是元素在 set 中的位置是有该元素的 hashCode 决定的,其位置其实是固定的)。
48. BIO、NIO、AIO 有什么区别?BIO
BIO全称是Blocking IO,是JDK1.4之前的传统IO模型,本身是同步阻塞模式。
线程发起IO请求后,一直阻塞IO,直到缓冲区数据就绪后,再进入下一步操作。针对网络通信都是一请求一应答的方式,虽然简化了上层的应用开发,但在性能和可靠性方面存在着巨大瓶颈,试想一下如果每个请求都需要新建一个线程来专门处理,那么在高并发的场景下,机器资源很快就会被耗尽。
NIO
NIO也叫Non-Blocking IO 是同步非阻塞的IO模型。线程发起io请求后,立即返回(非阻塞io)。同步指的是必须等待IO缓冲区内的数据就绪,而非阻塞指的是,用户线程不原地等待IO缓冲区,可以先做一些其他操作,但是要定时轮询检查IO缓冲区数据是否就绪。Java中的NIO 是new IO的意思。其实是NIO加上IO多路复用技术。普通的NIO是线程轮询查看一个IO缓冲区是否就绪,而Java中的new IO指的是线程轮询地去查看一堆IO缓冲区中哪些就绪,这是一种IO多路复用的思想。IO多路复用模型中,将检查IO数据是否就绪的任务,交给系统级别的select或epoll模型,由系统进行监控,减轻用户线程负担。
NIO主要有buffer、channel、selector三种技术的整合,通过零拷贝的buffer取得数据,每一个客户端通过channel在selector(多路复用器)上进行注册。服务端不断轮询channel来获取客户端的信息。channel上有connect,accept(阻塞)、read(可读)、write(可写)四种状态标识。根据标识来进行后续操作。所以一个服务端可接收无限多的channel。不需要新开一个线程。大大提升了性能。
AIO
AIO是真正意义上的异步非阻塞IO模型。
上述NIO实现中,需要用户线程定时轮询,去检查IO缓冲区数据是否就绪,占用应用程序线程资源,其实轮询相当于还是阻塞的,并非真正解放当前线程,因为它还是需要去查询哪些IO就绪。而真正的理想的异步非阻塞IO应该让内核系统完成,用户线程只需要告诉内核,当缓冲区就绪后,通知我或者执行我交给你的回调函数。
AIO可以做到真正的异步的操作,但实现起来比较复杂,支持纯异步IO的操作系统非常少,目前也就windows是IOCP技术实现了,而在Linux上,底层还是是使用的epoll实现的。
按值传递是指的是在方法调用时,传递的参数是按值的拷贝传递。按值传递重要特点:传递的是值的拷贝,也就是说传递后就互不相关了示例如下:
class TempTest {
private void test1(int a) {
a = 5;
System.out.println("test1 方法中的 a=" + a);
}
public static void main(String[] args) {
TempTest t = new TempTest();
int a = 3;
//传递后,test1 方法对变量值的改变不影响这里的 a
t.test1(a);
System.out.println("main 方法中的 a =" + a);
}
}
运行结果是:
test1 方法中的 a=5 main 方法中的 a =3
按引用传递是指的是在方法调用时,传递的参数是按引用进行传递,其实传递的是引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。示例如下:
class TempTest {
private void test1(A a) {
a.age = 20;
System.out.println("test1 方法中的 age=" + a.age);
}
public static void main(String[] args) {
TempTest t = new TempTest();
A a = new A();
a.age = 10;
t.test1(a);
System.out.println("main 方法中的 age =" + a.age);
}
}
class A {
public int age = 0;
}
运行结果:
test1 方法中的 age=20 main 方法中的 age =2050. static存在的主要意义
static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法!
static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。
为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
static的独特之处
1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩?
2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!
4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
static应用场景
因为static是被类的实例对象所共享,因此如果某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量。
因此比较常见的static应用场景有:
1、修饰成员变量
2、修饰成员方法
3、静态代码块
4、修饰类【只能修饰内部类也就是静态内部类】 5、静态导包
static注意事项
1、静态只能访问静态。
2、非静态既可以访问非静态的,也可以访问静态的。



