Java基础面试(String,泛型,关键字、IO)

写在前面:
先声明下,这个面试专题,主要是写给自己的,用来在挤公交的时候学习下,顺便做个分享。。。 我就是个小菜鸡。

Java基础

  • Java基础
    • 基本类型和包装类型
    • 缓存池
    • 泛型
      • 泛型擦除
    • String
      • 不可变的好处
      • JVM创建String的2中方法
      • subString方法
      • equals和compareTo
    • 关键字
      • final
      • static
    • 接口和抽象类
    • javaIO/NIO

Java基础

基本类型和包装类型

byte/8
char/16
short/16
int/32
float/32
long/64
double/64
boolean/~
boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。

包装类与基本类型的不同

  • 包装类型可以为 null,而基本类型不可以,尤其运用在pojo中
  • 包装类型可用于泛型,而基本类型不可以
  • 基本类型比包装类型更高效
    基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用
    Java基础面试(String,泛型,关键字、IO)_第1张图片

缓存池

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

new Integer(123) 每次都会新建一个对象;
Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。

在创建Integer对象,尽量少用new创建,尽量使用valueOf,利用整型池的提高系统性能,通过包装类的valueOf生成包装实例可以提高空间和时间性能。

基本类型对应的缓冲池如下:
boolean values true and false
all byte values
short values between -128 and 127
int values between -128 and 127
char in the range \u0000 to \u007F

泛型

泛型的本质是为了参数化类型。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型擦除

泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。

  • List和List 之间有什么区别 ?
    这两个List的声明都是限定通配符的例子,List可以接受任何继承自T的类型的List,而List可以接受任何T的父类构成的List。例如List可以接受List或List。

泛型的优点
1、类型安全
泛型的主要目标是提高Java程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在非常高的层次上验证类型假设。没有泛型,这些假设就只存在于系统开发人员的头脑中。
通过在变量声明中捕获这一附加的类型信息,泛型允许编译器实施这些附加的类型约束。类型错误就可以在编译时被捕获了,而不是在运行时当作ClassCastException展示出来。将类型检查从运行时挪到编译时有助于Java开发人员更早、更容易地找到错误,并可提高程序的可靠性。
2、消除强制类型转换
泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。尽管减少强制类型转换可以提高使用泛型类的代码的累赞程度,但是声明泛型变量时却会带来相应的累赞程度。在简单的程序中使用一次泛型变量不会降低代码累赞程度。但是对于多次使用泛型变量的大型程序来说,则可以累积起来降低累赞程度。所以泛型消除了强制类型转换之后,会使得代码加清晰和筒洁。
3、更高的运行效率
在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。

String

String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)

不可变的好处

  1. 可以缓存 hash 值
    因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

  2. String Pool(线程池)
    如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

  3. 安全性
    String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。

  4. 线程安全
    String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

JVM创建String的2中方法

  1. 直接赋值:String str = “apple”
    先去字符串常量池中检查是否有此值,如果有就把引用地址直接指向此值,若没有就再常量池中创建,然后在把引用指向此值。
  2. String str = new String();
    一定会现在堆上创建一个字符串对象,然后再去常量池中查询,如果不存在会先在常量池中创建此字符串,然后在把引用的值指向此字符串

subString方法

左闭右开的复制String
在1.6前产生OOM问题,因为是先复制String后拷贝;
1.7之后直接从原来的String拷贝

equals和compareTo

  1. equals会先判断是否是String类型,如果不是直接false;若是,则会循环比较所有字符,相同返回true
  2. compareTo,会循环比较所有的字符,如果有不相同,则返回char1-char2;所以返回0时,表示字符串相同

关键字

final

数据(不可改变);
方法(不能被子类重写);
类(不能被继承);

static

  1. 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
  2. 静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
  3. 静态语句块在类初始化时运行一次。

存在继承的情况下,初始化顺序为:

父类(静态变量、静态语句块)
子类(静态变量、静态语句块)
父类(实例变量、普通语句块)
父类(构造函数)
子类(实例变量、普通语句块)
子类(构造函数)

接口和抽象类

  1. 从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
  2. 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
  3. 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
  4. 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。

javaIO/NIO

  • 阻塞 IO 模型
    最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后,内
    核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用
    户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用
    户线程才解除 block 状态。典型的阻塞 IO 模型的例子为:data = socket.read();如果数据没有就
    绪,就会一直阻塞在 read 方法。
  • 非阻塞 IO 模型
    当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个
    error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备
    好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
    所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO
    不会交出 CPU,而会一直占用 CPU。典型的非阻塞 IO 模型一般如下:
while(true){
	data = socket.read();
	if(data!= error){
	处理数据
	break;
	}
}

但是对于非阻塞 IO 就有一个非常严重的问题,在 while 循环中需要不断地去询问内核数据是否就
绪,这样会导致 CPU 占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取数据。

  • 多路复用 IO 模型
    多路复用 IO 模型是目前使用得比较多的模型。Java NIO 实际上就是多路复用 IO在多路复用 IO
    模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真
    正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个
    socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有
    socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。在 Java NIO 中,是通
    过 selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这
    种方式会导致用户线程的阻塞。多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当
    socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连
    接数比较多的情况。
    另外
    多路复用 IO 为何比非阻塞 IO 模型的效率高
    是因为在非阻塞 IO 中,不断地询问 socket 状态
    时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效
    率要比用户线程要高的多。
    不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件
    逐一进行响应。因此对于多路复用 IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件
    迟迟得不到处理,并且会影响新的事件轮询。
  • 异步 IO 模型
    异步 IO 模型才是最理想的 IO 模型,在异步 IO 模型中,当用户线程发起 read 操作之后,立刻就
    可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,
    它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何 block。然后,内
    核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程
    发送一个信号,告诉它 read 操作完成了。也就说用户线程完全不需要实际的整个 IO 操作是如何
    进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接
    去使用数据了。
    也就说在异步 IO 模型中,IO 操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完
    成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用 IO 函数进行具体的
    读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据
    已经就绪,然后需要用户线程调用 IO 函数进行实际的读写操作;而在异步 IO 模型中,收到信号
    表示 IO 操作已经完成,不需要再在用户线程中调用 IO 函数进行实际的读写操作。

文章主要参考来自于github上的cs_notes,通过我面试考到的知识点,做了一个常考题目的汇总。
github连接

你可能感兴趣的:(面试)