关于封装的解释
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
字符型常量与字符串常量的区别
-
形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符
-
含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
占内存大小 字符常量只占2个字节; -
字符串常量占若干个字节 (注意: char在Java中占两个字节)
java基本类型所占存储空间大小
多态的实现方式
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
构造器 Constructor 是否可被 override?
Constructor 不能被 override , 但可以重载 overload,所以我们可以看到一个类中有多个构造函数的情况。
重载和重写的区别?
- 重载 overload 发生在同一个类中,方法名必须相同,参数类型,个数不同,顺序不同,方法返回值和访问修饰符可以不同。
- Java允许重载任何方法,不只是构造器。
- 重写是子类对父类的可访问方法的实现过程进行重新编写,发生在子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法的修饰符为private 则子类不能重写该方法。
Java 面向对象编程三大特性:封装、继承、多态
封装
封装把一个对象的属性私有化,同时提供一些可以被外界访问的方法。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
继承
继承是使用已存在的类的定义作为基础建立新类的技术,可以增加新的属性和功能。通过继承我们可以非常方便地复用以前的代码。
继承的3个需要注意的点er:
- 字类可以拥有父类的所有属性和方法(包括私有属性和方法),但是父类中的私有属性和方法是不能被访问的,只是拥有。
- 子类可以拥有自己属性和方法,也就是对父类进行拓展。
- 子类可以用自己的方式实现父类的方法。
多态
在java中有两种方式可以实现多态:继承(多个子类中对同一方法的重写)和接口(实现接口并覆盖接口中的方法)。
多态指的是程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行时才确定。 也即是 引用对象会指向哪个类的实例对象,引用变量发出的方法调用到底式哪个类中的方法,必须由程序运行时才能确定。
String StringBuffer和StringBuilder的区别是什么?String为什么是不可变的?
可变性
-
简单来说:String类使用final关键字修饰字符数组来保存字符串,private final char value[],所以String对象是不可变的。
-
而StringBuffer 和StringBuilder都继承自AbstractStringBuilder类,AbstractStringBuilder类中也是使用字符数组保存字符串char value[],但是没有使用关键字final,所以StringBuffer和StringBuilder都是可变的。
-
StringBuffer和StringBuilder的构造方法都是调用父类构造方法实现的。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
}
线性安全
- String对象是不可变的,所以是线性安全的。
- AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。
- StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线性安全的。StringBuilder没有对方法加同步锁,是非线性安全的。
性能
- 每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的对象。
- StringBuffer对对象本身进行操作,所以StringBuffer相比StingBuilder的性能高10%~15%,但线性不安全。
总结
- 操作量少的数据:使用String。
- 单线程操作字符串缓冲区下操作大量数据:StringBuiler。
- 多线程操作字符串缓冲区下操作大量数据:StringBuffer。
自动装箱与拆箱
- 装箱:把基本类型用它们对应的引用类型包装起来。
- 拆箱:把包装类型转换为基本数据类型。
在一个静态方法内调用一个非静态成员为什么是非法的?
由于静态方法可以不通过对象调用,因此在静态方法里,不能调用其它非静态变量,也不可以访问非静态变量成员。
在java中定义一个不做事且没有参数的构造方法的作用
Java 程序在执行子类的构造方法之前,如果没有用 super()
来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()
来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
接口和抽象类的区别是什么?
- 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为规范。
- 接口的方法默认是publi,所有方法在接口中不能实现(java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象方法。
- 接口中除了static,final变量,不能有其它变量,而抽象类不一定。
- 一个类可以实现多个接口,但只能实现一个抽象类。接口本身可以通过extends关键字拓展多个接口。
- 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。
成员变量和局部变量的区别有哪些?
- 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 从变量在内存中的存储方式来看:如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 - 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
- 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
对象实例和对象引用的区别
new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。
一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?
主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
构造方法有哪些特性?
- 名字与类名相同。
- 没有返回值,但不能用void声明构造函数。
- 生成类的对象时自动执行,无需调用。
静态方法和实例方法有何不同?
- 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
对象相等与指向它们的引用相等,二者有什么不同?
对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。
在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?
帮助子类做初始化工作。
== 与 equals(重要)
:它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型==比较的是内存地址)
equals():它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况2:类覆盖了equals()方法。一般,我们都覆盖equals()方法来比较两个对象的内容是否相等。若相等则返回true。
举个例子
public class Test1() {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b 为另一个引用,与a的内容一样
String aa = "ab"; // 放在常量池之中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
说明:
- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
hashCode 与 equals(重要)
注:面试官问你,“你重写过hashcode和equals么,为什么重写equals时必须重写hashCode方法?”
hashCode()简介
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有hashCode
我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()
方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
通过我们可以看出:hashCode()
的作用就是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()
在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
hashCode() 与 equals() 的相关规定
- 如果两个对象相等,则hashCode一定也是相同的。
- 两个对象相等,对两个对象分别调用equals方法都返回true。
- 两个对象有相同的hashCode值,它们也不一定相等。
- 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
- hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
推荐阅读:Java hashCode() 和 equals()的若干问题解答
为什么java中只有值传递?
简述线程、程序、进程的基本概念,以及它们之间的关系是什么?
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
线程有哪些基本状态
Java线程在运行的生命周期中的指定时刻只可能处于6中状态之中。
![Java线程的状态](E:\\求职\\java note\\images\\Java线程的状态.png)
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
![](E:\\求职\\java note\\images\\Java 线程状态变迁.png)
由上图可以看出:
线程创建之后它将处于 NEW(新建) 状态,调用 start()
方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运行) 状态。
操作系统隐藏 Java虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
当线程执行 wait()
方法之后,线程进入 WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或 wait(long millis)
方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()
方法之后将会进入到 TERMINATED(终止) 状态。
关于final关键字的总结
final关键字用到的三个地方:变量、方法、类。
- 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
- 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。
- 使用final方法的原因有两个。
- 一,把方法锁定,以防止任何继承类修改它的含义。
- 二,效率。类中所有的private方法都隐式地指定为final。
Java中异常处理
Java异常类层次结构图
![](E:\\求职\\java note\\images\\Exception.png)
在Java中,所有的异常类都有一个共同的祖先java.lang包中的Throwable类。Throwable类有两个子类Error和Exception。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重的问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本省可以处理的异常。 Exception类有一个重要的子类RuntimeException。
- RuntimeException异常由Java虚拟机抛出。
- NullPointerException:要访问的变量没有引用任何对象时,抛出该异常。
- ArithmeticException:算数异常错误。
- ArrayIndexOutOfBoundsException:下标越界异常。
注意:异常时能被程序本身处理,而错误是无法处理的。
Throwable类常用方法
- public string getMessage():返回异常时的简要信息。
- public string toString():返回异常时的详细信息。
- public string getLocalizedMessage():返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
- public void printStackTrace():在控制台上打印Throwable对象封装的异常信息
异常处理总结
- try 块: 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
- catch 块: 用于处理try捕获到的异常。
- finally 块: 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return
语句时,finally语句块将在方法返回之前被执行。
在以下4种特殊情况下,finally块不会被执行:
- 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行
- 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 ;若该语句在异常语句之后,finally会执行
- 程序所在的线程死亡。
- 关闭CPU。
注意: 当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会覆盖原始的返回值。如下:
public static int f(int value) {
try {
return value * value;
} finally {
if (value == 2) {
return 0;
}
}
}
如果调用 f(2)
,返回值将是0,因为finally语句的返回值覆盖了try语句块的返回值。
Java序列化中有些字段不想序列化怎么办?
对于不想序列化的变量,使用transient关键字修饰。
transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。
获取键盘操作的两种方法
方法1: Scanner
Scanner sc = new Scanner(System.in);
String s = sc.nextLine();
sc.close();
方法2:BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
Stirng s = input.nextLine();
Java中的IO流
Java中的IO流跟为几种?
- 按流的流向:输入流和输出流。
- 按操作单元:字节流和字符流。
- 按流的角色分:节点流和处理流。
Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。
- InputStream/Reader:所有的输入流的基类,前者时字节输入流,后者是字符输入流。
- OutputStream/Writer:所有的输出流的基类,前者时字节输出流,后者是字符输出流。
按操作方式分类结构图
![](E:\\求职\\java note\\images\\IO-操作方式分类.png)
按操作对象分类结构图:
![](E:\\求职\\java note\\images\\IO-操作对象分类.png)
既然有了字节流,干嘛还需要字符流?
问题本质想问:不管是文件读写还是网络发送接受,信息的最小存储单元都是字节,那为什么I/O流操作要分为字节操作流和字符操作流呢?
答:字符流是由Java虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时的,而且如果我们不知道编码类型就很容易出现乱码问题。所有,I/O流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行操作。如果音频文件,图片等媒体文件用字节流比较好,如果设计到字符的话还是用字符流操作比较好。
BIO,NIO,AIO 有什么区别?
BIO(Blocking I/O):同步阻塞IO模式,数据的读取写入必须阻塞在同一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO(new IO):是一种同步非阻塞的IO模型,在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,不过又放弃了。
常见关键字总结:Static,final,this,super
详见笔主的这篇文章: https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Basis/final、static、this、super.md
37. Collections 工具类和 Arrays 工具类常见方法总结
38. 深拷贝 vs 浅拷贝
- 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
- 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。