查缺补漏-JavaCore

本章是整理知识内容,为强化知识长期更新。

什么是面向对象?

  • 面向对象是一个思想,时间万物皆可以被看做一个对象。

介绍下 Java 基本数据类型

  • Java中存在8个原生数据类型,同时又分成四种:整形、浮点型、char、Boolean。它们之间存在自动类型转换,规则是从小到大。并且都存在自动装箱拆箱特性,但是这种操作是隐式操作而且在某些情况会导致CG压力增大。
类型 存储需求 取值范围
int 4字节 -2 147 483 638 ~ 2 147 483 637
short 2字节 -32 768 ~ 32 767
long 8字节 -9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807
byte 1字节 - 128 ~ 127
  • 整型的范围与运行Java运行的硬件没有关系,所有的数据类型所占的字节数量与平台无关。
类型 存储需求 取值范围
float 4字节 大约 $\pm$ 3.402 823 37F + 38F (有效位数为7~8位)
double 8字节 大约 $\pm$ 1.797 693 134 862 315 70E + 308 (有效位数为15位)
  • double这种类型的精度是float的两倍。
  • 所有浮点数值计算都遵循IEEE 754规范,下面是溢出和出错的情况的三种特殊的浮点数值。

    • 正无穷大
    • 负无穷大
    • NaN ( 不是一个数字 )
    • 一个整整数除以0的结果为正无穷大,计算0/0或者负数的平方根结果为NaN。
  • char类型

    • char类型表示单个字符,属于Unicode编码表。因为历史原因,不建议在程序中使用。除非确实要对UTF-16代码单元进行操作。
    • char字节大小
      • Java中无论是汉字还是英文字母都是用Unicode编码来表示的,一个Unicode是16位,每字节是8位,所以一个Unicode码占两字节。但是英文字母比较特殊,源自于8位(1字节)的ASCII吗,于是在Unicode码仅使用了低8位(1字节)就可以表示。
  • boolean类型

    • 布尔类型,只有两个值false、true。基本用于判定条件。
    • boolean字节大小
      • Java规范中并未明确规定boolean类型的大小。
  • 自动类型转换

    • 整型、实型(常量)、字符型数据可以混合运算。运算中,不同类型的数据先转化为同一类型,然后进行运算。

      转换从低级到高级。

      1
      byte,short,char—> int —> long—> float —> double
    • 不能对boolean进行类型转换、不能把对象类型转换成不相关的对象、把大容量的对象转换成小容量对象时需要强制类型转换、转换过程中间可能出现精度损失。

  • 装箱和拆箱boxing or unboxing

原语 对应的JDK类
int java.lang.Integer
short java.lang.Short
long java.lang.Long
byte java.lang.Byte
char java.lang.Character
double java.lang.Double
float java.lang.Float
boolean java.lang.Boolean
  • Java中只有原生数据类型是特殊的,它们不是对象。其它的都是对象。那么就一个尴尬的问题,集合类都是存放的对象,JDK5之后考虑到这个问题就自动进行逆行拆箱装箱的操作。

    • 1
      2
      3
      //比如所在泛型中是不能存放原生数据类型的,如要要存放原生数据类型的数据,需要装箱。
      Collection<int> c = new ArrayList<int>(); //这是无法编译成功的。
      Collection<Integer> cc = new ArrayList<Integer>(); //这样才行。
  • 每个 JDK 类都提供了相应方法来解析它的内部表示,并将其转换为相应的原语类型。

  • 但是注意装箱拆箱操作其实是非常消耗内存的举动,在该过程中可能会生成生成无用对象增加GC压力。所以尽量避免这中操作。

    1
    2
    3
    4
    Integer sum = 0;
    for(int index = 1000; index < 5000; index ++){
    sum+=index;
    }

    比如这种,每次sum都需要自动拆箱。

  • 默认情况下整数的类型都是int、浮点型的数都是double。

    • 隐式强制类型转换

    • 1
      float d = 1.1f; //在后面添加f,大小写不区分。

声明一个Double变量赋值 0.001会编译错误吗 ?

  • 不会,double 数据类型是双精度、64 位、标准的浮点数。

Java是纯粹的面向对象语言吗?

  • Java不是存粹的面向对象语言,因为它包含了原生数据类型,比如int 、double。

Java面向对象的特征?

  • 继承、封装、多态、抽象。

int 和 Integer 有什么区别。

  • Integer是int的包装类,它有一个int类型字段存储数据,并提供了基础的操作。关于Integer的缓存值,jdk5之后引入了一个静态工厂方法valueOf,在调用它的时候会有明显的性能提升,内部是一个缓存机制IntegerCache默认的长度是-128~127之间。
    • 这种缓存行为不仅适用于Integer对象。我们针对所有整数类型的类都有类似的缓存机制。
      • 有 ByteCache 用于缓存 Byte 对象。
      • 有 ShortCache 用于缓存 Short 对象。
      • 有 LongCache 用于缓存 Long 对象。
      • 有 CharacterCache 用于缓存 Character 对象。
      • Byte,Short,Long 有固定范围: -128 到 127。对于 Character, 范围是 0 到 127。除了 Integer 可以通过参数改变范围外,其它的都不行。

Java中访问类型有哪几种

访问类型 同一个类 同一个包 不同包的子类 不同包的非子类
public Y Y Y Y
protected Y Y Y
default Y Y
private Y

Java中重载和重写的区别

  • 重写(override)
    • 存在父类和子类之间。
    • 方法名、参数、返回值相同。
    • 方法被final修饰不能被重写。
    • 子类重写父类方法后,不能抛出比父类方法的异常。子类不能缩写父类的方法访问权限
  • 重载(overload)
    • 参数类型、个数、顺序至少有一个不相同。
    • 不能重载只有返回值不同的方法名。
    • 存在于父类和子类、同类中。

Java值传递和引用传递的区别

  • 其实只有传值

  • 值传递(call by value),对于基本型变量,传递的是该变量的副本,改变副本不影响变量。

  • 传递引用(call by reference),对于对象型变量,传递的该对象的地址的一个副本,并不是原对象本身。

String、StringBuffer、StringBuilder的区别

  • 可变性
    • String 是不可变的,StringBuffer\StringBuilder是可变的。
    • String 类中使用 final 关键字字符数组保存字符串,private final char value[],所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
  • 线程安全方面
    • String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilderStringBuilderStringBuffer 的公共父类,但是StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
  • 性能
    • 操作少量的数据 = String
    • 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
    • 多线程操作字符串缓冲区下操作大量数据 = StringBuffer

== 和 equals

  • == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
  • equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

    • 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
    • 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
    • String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
    • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

为什么要重写equals和hashcode方法

  • equals是Object的成员方法,默认不重写(override)情况下判断等价性。
    • 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
    • 类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class test {
public void test() {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
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");
}
}
}
  • hashCode hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。

    • 集合中使用场景。
      • 将对象放入到集合中时,首先判断要放入对象的 hashCode 值与集合中的任意一个元素的 hashCode 值是否相等,如果不相等直接将该对象放入集合中。
      • 如果 hashCode 值相等,然后再通过 equals 方法判断要放入对象与集合中的任意一个对象是否相等,如果 equals 判断不相等,直接将该元素放入到集合中,否则不放入。
  • hashCode()与equals()的相关规定

    1. 如果两个对象相等,则hashcode一定也是相同的。
    2. 两个对象相等,对两个对象分别调用equals方法都返回true。
    3. 两个对象有相同的hashcode值,它们也不一定是相等的。
    4. 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
    5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

描述下final 与 finally、finalize的区别。

  • final ,是修饰符关键字。

    • 如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在new一个对象时初始化(即只能在声明变量或构造器或代码块内初始化),而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能覆盖(重写)。

      • 父类的private成员方法是不能被子类覆盖的,因为被private修饰的方法默认是final类型的。
      • final类
        • final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会载被扩展,那么就设计为final类。
      • final方法
        • 如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。
      • final变量(常量)
        • 用final修饰的成员变量表示常量,值一旦给定就无法改变!
        • final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
        • final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。类中的final数据成员就可以根据依赖对象而有所不同,并保持其恒定不变的特征。
      • final参数

        • 当函数参数为final类型时,你可以读取使用该参数,但是无法改变该参数的值。
      • 注意final 不是immutable的

  • finally在异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。

    • 在异常处理时提供 finally 块来执行任何清除操作。只有在与 finally 相对应的 try 语句块得到执行的情况下,finally 语句块才会执行。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。
    • finally 语句块可能是要执行的。
      • 当try流程中出现程序中断情况是不会在执行finally语句的。也就是说一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行。还有更极端的情况,就是在线程运行 try 语句块或者 catch 语句块时,突然死机或者断电,finally 语句块肯定不会执行了。
  • finalize ,是方法名。

    • Java 允许使用 #finalize() 方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。

finally对应的try catch语句流程

1
2
3
4
5
6
7
8
9
10
11
   public static void main(String[] args)  {
try {
System.out.println("try block");
return ;
} finally {
System.out.println("finally block");
}
}
//运行结果
//try block
//finally block

从结果看finally语句会在return之前执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args)  {
System.out.println("return value of test(): " + test());
}

public static int test() {
int i = 1;
try {
System.out.println("try block");
i = 1 / 0; //抛一个异常进catch
return i;
}catch (Exception e){
System.out.println("exception block");
return 2;
}finally {
System.out.println("finally block");
}
}
//运行结果
//try block
//exception block
//finally block
//return value of test(): 2

从结果看finally 语句块在 catch 语句块中的 return 语句之前执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args)  {
System.out.println("return value of test(): " + test());
}

@SuppressWarnings("finally")
public static int test() {
int i = 1;
try {
i = 4;
return i;
} finally {
i++;
return i;
}
}
//运行结果
//return value of test(): 5

从结果看 finally 语句块中如果出现return那么该流程就结束了。其实是finally块中的return语句会覆盖try块中的return返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 public static void main(String[] args)  {
System.out.println("return value of test3(): " + test());
}

public static int test3() {
int b = 20;
try {
System.out.println("try block");
b += 80;
return b;
} catch (Exception e) {
System.out.println("catch block");
} finally {
System.out.println("finally block");
if (b > 25) {
b += 100;
System.out.println("b>25 and b = " + b);
}
}
return b;
}
// 运行结果
//try block
//finally block
//b>25 and b = 200
//return value of test(): 100

从结果看:weary:为什么不返回200!为什么finally里面的修改没有效果?因为finally语句中没有return语句覆盖返回值,那么原来的返回值可能因为finally里的修改而改变也可能不变。那什么情况下会改变呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 public static void main(String[] args)  {
System.out.println("return value of test3(): " + test());
}

public static int test3() {
int b = 20;
try {
System.out.println("try block");
b += 80;
b = b / 0; //抛异常
return b;
}
catch (Exception e) {
System.out.println("catch block");
}
finally {
System.out.println("finally block");
if (b > 25) {
b += 100;
System.out.println("b>25 and b = " + b);
}
}
return b;
}
//运行结果
//try block
//catch block
//finally block
//b>25 and b = 200
//return value of test(): 200

从结果看finally里面的修改启效果了,因为抛出了异常所以没有执行try代码块里面的return。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    public static void main(String[] args)  {
System.out.println("return value of test3(): " + test());
}
public static int test3() {
int b = 20;
try {
System.out.println("try block");
b += 80;
b = b / 0;
return b;
}
catch (Exception e) {
System.out.println("catch block");
return b += 10;
}
finally {
System.out.println("finally block");
if (b > 25) {
b += 100;
System.out.println("b>25 and b = " + b);
}
}
}
// 运行结果
// try block
// catch block
// finally block
// b>25 and b = 210
// return value of test(): 110
// 从结果来看,抛出异常后return 方法执行之前,也运行了finally代码块,但是并未影响catch代码块中的返回值。
  • finally块的语句在try或catch中的return语句执行之后返回之前执行,finally里的修改语句可能影响也可能不影响try或catch中 return已经确定的返回值,若finally里也有return语句则覆盖try或catch中的return语句直接返回。没有进入try代码块就不会执行finally代码块。

介绍下异常类型

  • Throwable
    • Error
    • Exception
      • RuntimeException
      • IOException
  • 超类Throwable ,有两个子类Error和Exception,分别表示错误和异常。
    • Error是程序无法处理的错误,比如OutOfMemoryError等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
    • Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。
      • 运行时异常(RuntimeException)和非运行时异常也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception),这两种异常有很大的区别。
      • RuntimeException(运行时异常),表示无法让程序恢复的异常,导致的原因通常是因为执行了错误的操作,建议终止逻辑,因此,编译器不检查这些异常。
      • CheckedException(受检查异常),是表示程序可以处理的异常,也即表示程序可以修复(由程序自己接受异常并且做出处理),所以称之为受检查异常。

描述下异常处理方式

  • 使用System.out.println是高代价的,这这做会降低系统吞吐量。
  • 在生成环境中避免使用printStackTrace()方法,printStackTrace默认会把调用的堆栈打印到控制台上,在生产环境中访问控制台是不现实的。
  • 如果不能处理异常,就不要捕获该异常,
  • 如果要捕获异常,应在最近的地方捕获它。
  • 不要吃掉你的捕捉的异常信息,就是捕获了啥也不做,建议LOG记录。
  • 不要将异常处理用于正常的控制流(设计良好的 API 不应该强迫它的调用者为了正常的控制流而使用异常)。
  • 对可以恢复的情况使用受检异常,对编程错误使用运行时异常。
  • 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)。
  • 优先使用标准的异常。
  • 每个方法抛出的异常都要有文档。
  • 保持异常的原子性
  • 不要在 catch 中忽略掉捕获到的异常。

throw 与 throws 的区别

  • throw ,用于在程序中显式地抛出一个异常。
  • throws ,用于指出在该方法中没有处理的异常。每个方法必须显式指明哪些异常没有处理,以便该方法的调用者可以预防可能发生的异常。最后,多个异常用逗号分隔。

常见的几种异常

  • NullPointerException
  • IndexOutOfBoundsException
  • ClassCastException
  • ArrayStoreException
  • BufferOverflowException

如何正确的在一个循环中删除ArrayList中的元素。

  • 如果使用普通for循环直接删除会出现IndexOutOfBoundsException异常,非法索引。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    ArrayList<String> list = new ArrayList<String>();
    list.add("1");
    list.add("2");
    list.add("2");
    list.add("2");
    list.add("3");
    list.add("4");
    list.add("5");

    // 这样做肯定抛异常,非法访问数据越界
    int len = list.size();
    for (int i = 0; i < len; i++) {
    if("1".equals(list.get(i))){
    list.remove(i);
    }
    }

    //Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 4, Size: 4
    //改进后,删除元素后更新List长度,更新循环下标。但是这样的可读性很差。而且不适用于多线程场景
    int len = list.size();
    for (int i = 0; i < len; i++) {
    if("1".equals(list.get(i))){
    list.remove(i);
    --len;
    --i;
    }
    }
    //另外一种方式,这种看起来好读多了。。。
    Iterator<String> sListIterator = list.iterator();
    while(sListIterator.hasNext()){
    String e = sListIterator.next(); //注意了
    if(e.equals("1")){
    sListIterator.remove();
    }
    }

什么是 Java 序列化?

  • 序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。
  • 反序列化的过程,则是和序列化相反的过程。
  • 对于不想进行序列化的变量,使用transient关键字修饰。
    • transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。

介绍Java 反射

  • 反射(Reflection)是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。
  • 通过反射,可以在程序运行时访问Java对象的成员变量、方法、构造方法。
  • 反射的缺点
    • 性能开销 - 由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能要比非反射操作的性能要差,应该在性能敏感的应用程序中频繁调用的代码段中避免。
    • 破坏封装性 - 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
    • 内部曝光 - 由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。

Sleep 和 wait的区别

  • sleep是Thread的成员方法,睡眠时保持对象锁,仍然占有该锁。
    • sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
         sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
        在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
  • wait是Object的成员方法睡眠时,释放对象锁。
    • wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
        wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
        wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。

notify与notifyAll的区别

尽量使用notifyAll。

  • 调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
  • notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。