2.1.9面向对象的三大特性(携程):
封装:把一个对象的属性隐藏在对象内部,外部对象不能直接访问这个对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就比如我们常常创建一个类,把他的属性设置为private修饰符,get、set方法设置为public修饰符。
继承:继承是从已有的类中派生出新的类。
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以调用,重写父类非私有的属性和方法。
父类是子类的抽象化,而子类反过来就是父类的具体化。
多态:引用变量指向的具体实例和引用变量调用的方法是哪个类中实现的方法在编译时是不确定的。具体表现为父类的引用指向子类的实例。
多态的特点:
1.引用变量和具体实例之间具有继承(类)/实现(接口)的关系;
2.引用变量调用的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
3.多态不能调用“只在子类存在但在父类不存在”的方法;
4.如果子类重写了父类的方法,真正执行的是子类覆盖的方法,否则执行的是父类的方法。
2.1.10String、StringBuffer、StringBuilder区别?String为什么是不可变的?
1、可变性
String类的底层是private final char value[],因为使用了final关键字修饰,所以string对象是不可变的。
StringBuffer与StringBuilder都继承自AbstractStringBuilder类,他们的构造器都是调用了父类的构造器,在AbstractStringBuilder中也是使用字符数组保存字符串,但是没有用final关键字修饰,所以这两种对象都是可变的。
2、线程安全性
String中的对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer与StringBuilder都继承自AbstractStringBuilder类,定义了一些字符串的基本操作,如append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
3、性能
每次对String 类型进行改变的时候,都会生成一个新的String 对象,然后将指针指向新的String 对象。
StringBuffer和StringBuilder每次都会对对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
总结:
1.操作少量数据:适用String
2.单线程操作字符串缓冲区下操作大量数据:适用StringBuilder
3.多线程操作字符串缓冲区下操作大量数据:适用StringBuffer
2.1.11自动装箱与拆箱:
装箱:将基本类型用它们对应的引用类型包装起来;Integer.valueOf(int i)
拆箱:将包装类型转换为基本数据类型;intValue() xxxValue()
public class Main {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
true false
在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
注意,Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。
Double、Float的valueOf方法的实现是类似的。
当 "=="运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。
2.1.12在⼀个静态⽅法内调⽤⼀个⾮静态成员为什么是⾮法的?
类的静态变量和静态方法属于类本身,在类加载的时候就会分配内存,可以通过类名直接访问。
所以,一个类的静态方法调用非静态成员时,因为类的静态方法存在的时候类的非静态成员可能不存在,访问一个不存在的东西当然出错。
2.1.13在 Java 中定义⼀个不做事且没有参数的构造⽅法的作⽤?(创建无参构造器的作用)
子类构造方法一定调用父类的构造方法,如果没有用super()来调用父类特定的构造方法,则会默认调用父类的空参构造方法。而如果父类中定义了有参构造器则不会自动生成无参构造,super()找不到父类的特定构造方法会编译发生错误,因为父类没有无参构造方法。解决办法就是父类加上空参构造。
2.1.14接口和抽象类的区别:(超高频)
1、接口的方法默认public,抽象类的方法可以有public、protected、default不写(抽象方法就是为了被重写所以不能使用private修饰)。
如果是jdk1.7,接口中可以包含常量和抽象方法。1.8新增默认方法和静态方法,静态方法不能通过接口的实现类调用接口的静态方法。jdk9新增私有方法和私有静态方法解决重复代码的问题。而抽象类可以有非抽象的方法。
2、接口中只能有常量,默认public static final修饰,而抽象类不一定。
3、接口不能有构造方法,抽象类有。
3、一个类可以实现多个接口,但只能继承一个抽象类。
4、从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
2.1.15成员变量与局部变量的区别有哪些?
1、从语法形式看:成员变量属于类,局部变量是在方法中定义的变量或者是方法的参数。
成员变量可以被public、private、static等修饰,局部变量不能被访问控制修饰符及static修饰。但是成员变量和局部变量都能被final修饰。
2、从变量在内存中的存储方式看:成员变量被static修饰,成员变量属于类,否则属于实例。对象存于堆里面,如果局部变量是基本数据类型,存储在栈里面,如果是引用类型,存放的是指向堆内存对象的引用或者指向常量池中的地址。
3、从变量在内存中生存的时间上看:成员变量是对象的一部分,随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
4、成员变量如果没有被赋初值,会有默认值(final修饰的成员变量必须显式的赋值),而局部变量不会自动赋值。
2.1.19构造方法有哪些特性?
1、名字和类名相同。
2、没有返回值,并且不能用void修饰。
3、生成类的对象时自动执行,无需调用。
2.1.22在调⽤⼦类构造⽅法之前会先调⽤⽗类没有参数的构造⽅法,其⽬的是?
给子类初始化。
因为子类继承父类后,子类就有了很多父类的变量,所以要先调用父类的构造方法去帮子类初始化。
2.1.23==与equals(超高频):
==:基本类型比较值相等,引用类型比较地址相等。
equals:分两种情况:
1、类没有覆盖重写equals方法。等价于==
2、类override了equals方法。一般,我们都覆盖重写equals方法来比较两个对象内容是否相等。
举例:Object类中的equals方法是==。String类中的equals方法是被重写过的。
2.1.24hashCode与equals(超高频):
1、hashCode()介绍:hashCode()的作用是获取哈希码,返回一个int型的整数。用于确定该对象在哈希表中的索引位置。
2、为什么要有hashCode():
以HashSet如何检测重复为例子。
把对象加入到HashSet中,先计算有没有相同的hashCode,如果没有相同的hashcode,HashSet会假设对象没有重复出现。如果有hashcode值相同的对象,则调用equals()方法检查是否真的相同。如果真的相同,HashSet就不会加入。如果不同,就重新散列到其他位置。大大减少equals的次数,提高执行速度。
3、为什么重写equals方法必须重写hashCode方法?(hashCode与equals的区别)
两个对象相同,则hashCode肯定相同,调用equals方法返回true。
如果两个对象的hashcode值相同,两个对象也不一定相等。我们要遵循这个规则,因此,equals方法被重写,hashcode也必须重写。
4、为什么两个对象有相同的hashcode值,它们也不一定相等?
hashCode()所使用的杂凑算法也许刚好会让多个对象传回相同的hashcode值。越糟糕的算法越容易发生碰撞。所以同样的hashcode有多个对象,会使用equals方法判断是否真的相同。hashcode只是用来缩小查找成本。
5、场景
如果需要将自定义类对象作为HashMap的key,那么自定义类中需要重写hashCode()和equals()
2.1.25Java中只有值传递(超高频,理解):
一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)
一个方法可以改变一个对象参数的状态,原因是形参拷贝了实参的地址作为副本,共用一个地址,所以会改变堆中对象的属性值。
一个方法不能让对象参数引用一个新的对象。
2.1.26线程、程序、进程的基本概念,以及他们之间的关系:
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或者在各个线程之间切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。线程是cpu调度和分派的基本单位。
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的一次执行过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着。同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
线程是进程划分的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响,共享资源。线程执行开销小,但不利于资源的管理和保护;而进程正相反。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
2.1.27线程有哪些基本状态:
Java线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态:
NEW:初始状态,线程被构建,但还没有调用start()方法。
RUNNABLE:运行状态,Java线程将操作系统中的就绪(READY)和运行(RUNNING)两种状态笼统地称作"运行中"
BLOCKED:阻塞状态,表示线程阻塞于锁。【当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到BLOCKED阻塞状态】
WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(通知或中断)才能够返回到RUNNABLE运行状态。【当线程执行wait()方法之后,线程进入WAITING等待状态】
TIME_WAITING:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的。【相当于在WAITING等待状态的基础上增加了超时限制,比如通过sleep(long millis)方法或wait(long mills)方法可以将Java线程置于TIME_WAITING超时等待状态。当超时时间到达后Java线程将会返回到RUNNABLE状态。】
TERMINATED:终止状态,表示当前线程已经执行完毕。【线程在执行Runnable的run()方法之后将会进入到TERMINATED终止状态】
2.1.28关于final关键字的一些总结
final关键字主要用于三个地方:类、方法、变量。
1.final修饰类,表明这个类不能被继承,也就所有成员方法都无法进行override覆盖重写。
2.final修饰方法,这个方法不能被覆盖重写,防止任何继承类修改它的含义。
ps:对于类、方法来说,abstract和final关键字互相矛盾,不能同时使用。
3.final修饰变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
2.1.29.3Java中的异常处理
finally块:⽆论是否捕获或处理异常, finally 块⾥的语句都会被执⾏。当在 try 块或catch 块中遇到 return 语句时, finally 语句块将在⽅法返回之前被执⾏。
以下3种特殊情况下,finally块不会被执行:
1.在try或finally块中用了System.exit(int) 退出程序。但是,如果System.exit(int) 在异常语句之后,finally还是会被执行。
2.程序所在的线程死亡。
3.关闭CPU。
注意:在遇到try或catch的return语句前,先执行finally。
2.1.30Java序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用transient关键字修饰。
transient关键字的作用:使用transient关键字修饰,这个字段不会被序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。 transient 只能修饰变量,不能修饰类和⽅法。
2.1.31获取键盘输入常用的两种方法
方法1.通过Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close;
方法2.通过BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readerLine(); 这里有IOException要处理
2.1.32Java中IO流
Java IO流共涉及40多个类,都是从如下4个抽象类基类中派生出来的。
InputStream、Reader:所有的输⼊流的基类,前者是字节输⼊流,后者是字符输⼊流。
OutputStream、Writer:所有的输出流的基类,前者是字节输出流,后者是字符输出流。
2.1.32.2既然有了字节流,为什么还要有字符流?
问题本质想问: 不管是⽂件读写还是⽹络发送接收,信息的最⼩存储单元都是字节,那为什么I/O 流操作要分为字节流操作和字符流操作呢?
字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是⾮常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。如果⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐较好,如果涉及到字符的话使⽤字符流⽐较好。
2.1.32.3BIO,NIO,AIO区别?
UNIX 系统下, IO 模型一共有 5 种: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
Java 中 3 种常见 IO 模型:
BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写⼊必须阻塞在⼀个线程内等待其完成。在活动连接数不是特别⾼(⼩于单机 1000)的情况下,这种模型是⽐较不错的,可以让每⼀个连接专注于⾃⼰的 I/O 并且编程模型简单,也不⽤过多考虑系统的过载、限流等问题。线程池本身就是⼀个天然的漏⽃,可以缓冲⼀些系统处理不了的连接或请求。但是,当⾯对⼗万甚⾄百万级连接的时候,传统的 BIO 模型是⽆能为⼒的。因此,我们需要⼀种更⾼效的 I/O 处理模型来应对更⾼的并发量。
NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型,在 Java 1.4 中引⼊了NIO 框架,对应 java.nio 包,提供了 Channel , Selector, Buffer 等抽象。 NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它⽀持⾯向缓冲的,基于通道的 I/O 操作⽅法。NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和ServerSocketChannel 两种不同的套接字通道实现,两种通道都⽀持阻塞和⾮阻塞两种模式。阻塞模式使⽤就像传统中的⽀持⼀样,⽐较简单,但是性能和可靠性都不好;⾮阻塞模式正好与之相反。对于低负载、低并发的应⽤程序,可以使⽤同步阻塞 I/O 来提升开发速率和更好的维护性;对于⾼负载、⾼并发的(⽹络)应⽤,应使⽤ NIO 的⾮阻塞模式来开发
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引⼊了 NIO 的改进版 NIO 2,它是异步⾮阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应⽤操作之后会直接返回,不会堵塞在那⾥,当后台处理完成,操作系统会通知相应的线程进⾏后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在⽹络操作中,提供了⾮阻塞的⽅法,但是 NIO 的 IO ⾏为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程⾃⾏进⾏ IO 操作, IO 操作本身是同步的。查阅⽹上相关资料,我发现就⽬前来说 AIO 的应⽤还不是很⼴泛, Netty 之前也尝试使⽤过 AIO,不过⼜放弃了。
2.1.33深拷贝vs浅拷贝
1.浅拷贝:对基本数据类型进行值传递,对引用数据类型只拷贝地址。
2.深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容。