Java 面试(一)– 基础知识总结
本文根据 Java Guide
面向对象和面向过程的区别
- 面向对象的性能比面向过程的性能低
- 因为类调用得初始化、实例化,比较消耗资源
- Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码,而是交由jvm处理。
Java 语言有哪些特点?
- Java 三大特性(封装、继承、多态)
- jvm 实现跨平台特性
- 支持多线程和网络编程
- 编译和解释并存
- 编译器是把源程序的每一条语句都编译成机器语言,并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;
- 而解释器则是只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的.
- Java 比较特殊是先编译成字节码(.class)文件并不是其他语言编译后的二进制文件,然后在 jvm 上解释执行字节码。
- .class 字节码 –> 机器码,jvm 逐行解释会比较慢,所以引入了 JIT 编译器,JIT 完成一次编译后会将字节码对应的机器码保留下来,jvm 会根据每次执行的情况收集信息进行优化,所以越执行越快。
- jvm 是实现 “一次编译,到处运行”的关键
jdk、jre、jvm 的关系
- jdk 包括:
- javac(编译器)
- javadoc(生成java文档)
- jdb(命令行下debug)
- jre 包括:
- jvm
- java类库
- javadoc 图例:
- 如果你只想运行 java 程序的话 jre 就够了,但是基本上还是得需要 jdk
- HotSpot VM 是 Oracle JDK 和 Open JDK 共同的虚拟机,即 jvm
- Oracle JDK 基于 Open JDK,但是更稳定,性能更好
Java 与 c++ 的区别于联系
- 都是面向对象,封装、继承、多态
- Java 不提供指针访问内存,内存更安全
- Java 类是单继承,c++可以多继承
- Java 有内存自动管理机制,无需手动释放内存
字符型常量和字符串常量的区别
- 形式上:
- char :单引号引起来的一个字符
- string:双引号引起的多个字符
- 含义:
- char:字符型常量相当于一个整型值(ASCII值)
- string:代表一个地址值(即该字符串在内存中的位置)
- 占内存大小:
- char:2字节
- string:最少一字符(结束标志)
构造器 Constructor 不能被 override
重载和重写
- 重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
- 重写: 父类方法访问修饰符为 private 则子类就不能重写该方法。也就是说方法提供的行为改变,而方法的外貌并没有改变。
Java 面向对象三大特性:封装、继承、多态
继承
- 继承: 继承是使用已存在的类的定义作为基础建立新类的技术, 通过使用继承我们能够非常方便地复用以前的代码。子类用父类的功能或者自己增加新的功能,但不能选择性的继承。
- 1.子类拥有父类的所有方法和属性(包括私有属性和方法),但是无法访问,只是拥有
- 2.子类可以有自己的属性和方法,即可以对父类扩展
- 3.子类可以用自己的方式实现父类的方法(重写)
- 解释:
- 子类继承父类,子类拥有父类所有的属性和方法,但是父类的私有属性和私有方法,子类是不能访问的,当然一些父类的私有属性可能可以通过相应的方法访问到,但是私有的方法似乎不能简单的访问,这里暂不考虑Java反射机制。在一个子类被创建的时候,首先会在内存中创建一个父类对象,然后在父类对象外部放上子类独有的属性,两者合起来形成一个子类的对象。子类对象确实拥有父类对象中所有的属性和方法,但是父类对象中的私有属性和方法,子类是无法访问到的,只是拥有,但不能使用。所以子类对象是绝对大于父类对象的,所谓的子类对象只能继承父类非私有的属性及方法的说法是错误的,可以继承,只是无法访问到而已
封装
- 封装:把这个对象的属性私有化,然后对外提供可以被外界访问属性的方法。
多态
- 多态: 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
就是在编程期间,并不知道这个引用变量指向的具体类型是什么,或者是方法该调用哪个,只有在程序运行期间才能确定。多态的两种实现方式:- 继承(多个子类对一个方法的重写)
- 实现接口(实现接口并覆盖同一方法)
String、StringBuffer、StringBuilder 的区别?为什么String 是不可变的?
- String 源码中,用 private final char value[] 保存字符,因为 final 所以是不可变的
- StringBuffer 和 StringBuilder 都是继承 AbstractStringBuilder 类,而在这个类中 char value[] 保存字符,所以这两个可以改变
- StringBuffer 和 StringBuilder 都是调用者 AbstractStringBuilder 的构造函数:
1 | abstract class AbstractStringBuilder implements Appendable, CharSequence { |
- 线程安全性:
- String 中的对象是不可变的,可以理解为常量,所以线程安全
- AbstractStringBuilder 是这俩的公共父类,定义了一些对字符串操作的方法
- StringBuffer 对方法加了同步锁,所以线程安全
- StringBuilder 并没有加锁,所以非线程安全
- 性能:
- String 每次改变时都会生成一个新的对象,然后指针指向这个新的对象
- StringBuffer 、StringBuilder 都是对自己本身操作,不会产生新的对象来改变对象引用,StringBuilder 性能要比 StringBuffer 高 10%~15%,但是要冒着线程不安全的风险。
- StringBuilder > StringBuffer > String(性能)
- 总结:
- 操作少量数据:String
- 单线程下大量数据:StringBuilder
- 多线程下大量数据:StringBuffer
自动装箱、拆箱
装箱:将基本类型用它们对应的引用类型包装起来
拆箱:将包装类型转换为基本数据类型
在一个静态方法内调用一个非静态成员为什么是非法的?
- 静态方法属于类的本身,在类加载的时候就会分配内存,而非静态成员只有在类实例化时才会分配内存,静态方法不通过对象来调用,所以静态方法里,不能调用非静态成员
在 java 中定义一个不做事且没有参数的构造方法的作用:
- java 程序在执行子类构造方法之前会执行父类的构造方法,每个构造方法都隐含着 super() ,如果父类只定义了有参的构造函数没有提供 无参的构造方法,则会报错
import java和javax有什么区别?
- 以前 javax 是 java 包 的扩展,随着时间推移 javax 也成为 java api 的一部分,所以这俩现在没啥区别
接口和抽象类的区别?
- 接口方法默认是 public ,java 8 开始接口方法可以有默认实现(default static),而抽象类也可以包含非抽象的方法
- 接口中的变量一定是 static final 的,抽象类中随意
- 抽象方法就是为了重写所以别定义成 private,剩下的随意
- 设计层面来看:
- 抽象就是类的一种抽象,类似一个模板
- 而接口时类的行为的规范
成员变量与局部变量的区别?
语法上看
- 成员变量属于类,可以用访问控制符及 static 修饰
- 局部变量属于方法,不可以被上面这些修饰,但是两者都可以被 final 修饰
在内存的存储方式
- 成员变量 static 修饰,是属于类的
- 没有被 static 修饰,是属于实例对象的
- 对象存在 堆内存,局部变量存在 栈内存
内存生命周期
- 成员变量随对象的创建而存在,是对象的一部分
- 局部变量随方法的调用而自动消失
初值
- 成员变量会自动赋初值(final除外)
- 局部变量不会
对象实体和对象引用的不同?
- new 创建对象实体,对象实体存在 堆内存 中
- 对象引用指向对象实体,存在 栈内存 中
- 对象引用可以指向 0 ~ 1 个
- 对象实体对象实体可以被 n 个对象引用
静态方法 和 实例方法 有何不同?
- 静态方法属于类,不需要创建对象,里面只能访问静态成员变量,不能访问非静态的成员和方法
对象的相等 与 指向他们的引用相等 有何区别?
- 对象相等:比较的是内存中存放的内容是否相等
- 对象引用的相等:比较的是指向的内存地址是否相等
== 与 equals !!
==
- 比较的是内存地址,即两个对象是否一样,基本类型比较值,引用类型比较对象
equals
其实就是提供一个能够供类重写的一个方法:
- 没有重写的话与“==”一样比较内存地址
- 重写的话,比如 String 重写了 equals 比较的是内容是否一样,有自己的比较规则
hashCode 和 equals
hashCode()
- 作用是获取 哈希码(散列码),确定该对象在哈希表中的位置,hashCode() 在 Object.java 类中
- 以 hashSet 如何检查重复来说:
- 首先 通过 对象的 hashCode 来计算对象加入的位置,同时向其他对象的 hashCode 比较
- 如果没有则不重复,如果有 则调取对象的 equals 方法,再次判断内容是否相同
- 如果真相同,那么 hashSet 不会让他加入
- 这样的好处是减少了 equals 的使用次数,提高了运行速度
- hashMap 与这类似 不过它 不允许 键 重复,hashSet 不允许 值 重复
- hashCode 就是获取散列码,所以只在散列表中有用,在其他情况下没用
hashCode 和 equals 的规定
- 如果两个对象相同,则 hashCode 一定相同
- 两个对象的 hashCode 一样,也不一定相等(因为如果重写了 hashCode 的话,根据算法可能出现重复现象)
- 如果重写了 equals 方法那就必须得重写 hashCode 使得,equals返回 true 时,hashCode 必须返回 true
- 如果没重写过 hashCode 则不同对象的值永远不一样
Java只有值传递!!
1 | 传递对象的引用不会改变,但是对象的属性可以改变。 |
- 解释:
- 如果是引用传递,那么 change1 的时候,会把 list 对象传进来,然后 new ArrayList
() 的时候,就会赋予 list 一个新的指向的地址,那么应该输出的是什么都没有,因为指向了新的内存空间。 - 而正因为是值传递,值传递就是复制自己一份传给形参,对象的话就会复制自己的内存地址给了 list 形参,然后这个 list 在 指向 new 之后,是不会影响到原本的 list 的,所以这里还是输出了 1,2
- change2 就很好的说明了这点,因为传递的内存地址,所以 list.add 也确实加到了 实参 list 上面,所以输出 1,2,3
- 如果是引用传递,那么 change1 的时候,会把 list 对象传进来,然后 new ArrayList
- 上面这幅图如何解释?
- 既然是传内存地址为何没变
- 因为大家都忽略了,String = “hollischuang” 时,如果常量池里没有的话,就相当于 new String(“ hollischuang”)
- 所以也是指向新的内存地址,原本的不会改变
线程、程序、进程的基本概念,三者关系是什么?
线程
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
程序
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程
进程就是程序的一次执行过程,是系统运行程序的基本单位, 因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
线程的基本状态
- 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下:
- 线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运行) 状态。
- 操作系统隐藏 Java虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态**,所以 Java 系统一般将这两个状态统称为** RUNNABLE(运行中) 状态 。
- 当线程执行 wait()方法之后,线程进入 WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。
final 关键字总结
- 用在三个地方:变量、方法、类
变量
- 基本类型:则其数值初始化后不能改变
- 引用类型:初始化后不能指向其他对象
方法
- 以前版本的原因:
- 1.防止这个方法被继承
- 2.提高效率,final 方法会转为内嵌调用,但是如果方法过于庞大,就几乎没有性能提升
- 3.现在Java版本不需要这样优化了,private 定义的方法默认为 final,final定义属性的话没啥用,因为属性就不能被更改了
类
- 即这个类不能被继承,里面所有方法默认为 final
Java的异常处理
- 在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 Throwable类。Throwable: 有两个重要的子类:Exception(异常) 和 Error(错误) ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。
- Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
- Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 异常由Java虚拟机抛出。NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、ArithmeticException(算术运算异常,一个整数除以0时,抛出该异常)和 ArrayIndexOutOfBoundsException (下标越界异常)。
- 异常和错误的区别:异常能被程序本身处理,错误是无法处理。
Throwable 常用方法
- public string getMessage():返回异常发生时的简要描述
- public string toString():返回异常发生时的详细信息
- public string getLocalizedMessage():返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
- public void printStackTrace():在控制台上打印Throwable对象封装的异常信息
final 会覆盖 try 的 return 语句
Java 如果有些字段不想序列化怎么办?
- transient 字段来修饰变量,不能修饰类和方法
- ArrayList 就用到了 transient 来只序列化有效元素,来提高效率
- ArrayList 源码:
- 这里只把有效的元素序列化,提高效率
获取键盘输入的两种方法:
- Scanner
- BufferedReader
Java I/O 总结
- 按照流向:输入流、输出流
- 按照操作单元:字节流、字符流
- 按照流的角色:节点流、处理流
- Java IO 40 多个类,全部诞生于这 4 个类:
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
- 自己总结一下,带 Stream 的都是字节流,不带的 都是字符流
不同的读取方式的速度:批处理带缓冲>批处理>缓冲>直接读取
既然有了字节流为什么还有字符流?(主要是应对编码问题)
- 字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
BIO、NIO、AIO的区别?
BIO
BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO
NIO (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
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,不过又放弃了。
深拷贝和浅拷贝:
深拷贝
对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
浅拷贝
对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。