美文网首页Java程序员面试复习手册
Java基础知识点面试手册(上)

Java基础知识点面试手册(上)

作者: 蛮三刀酱 | 来源:发表于2019-01-21 14:12 被阅读0次

    前言

    本文快速回顾了Java中最基础的知识点,用作面试复习,事半功倍。

    参考

    基础知识点

    面向对象的特性

    答:封装、继承和多态。

    多态分为编译时多态和运行时多态。

    • 编译时多态:方法的重载
    • 运行时多态:指程序中定义的对象引用所指向的具体类型在运行期间才确定。

    运行时多态有三个条件

    多态的存在有三个前提:

    • 要有继承关系
    • 子类要重写父类的方法
    • 父类引用指向子类对,

    父类Animal

    class Animal {
        int num = 10;
        static int age = 20;
        public void eat() {
            System.out.println("动物吃饭");
            }
        public static void sleep() {
            System.out.println("动物在睡觉");
            }
        public void run(){
            System.out.println("动物在奔跑");
           }
    }
    

    子类Cat

    class Cat extends Animal {
        int num = 80;
        static int age = 90;
         String name = "tomCat";
        public void eat() {
            System.out.println("猫吃饭");
        }
        public static void sleep() {
            System.out.println("猫在睡觉");
        }
        public void catchMouse() {
            System.out.println("猫在抓老鼠");
        }
    }
    

    测试类Demo_Test1

    class Demo_Test1 {
        public static void main(String[] args) {
        Animal am = new Cat();
        am.eat();
        am.sleep();
        am.run();
        //am.catchMouse();这里先注释掉,等会会说明
         //System.out.println(am.name);//这里先注释,待会说明
        System.out.println(am.num);
        System.out.println(am.age);
        }
    }
    

    以上的三段代码充分体现了多态的三个前提,即:

    1、存在继承关系
    
    Cat类继承了Animal类
    
    2、子类要重写父类的方法
    
    子类重写(override)了父类的两个成员方法eat(),sleep()。其中eat()是非静态的,sleep()是静态的(static)。
    
    3、父类数据类型的引用指向子类对象。
    

    如果再深究一点呢,我们可以看看上面测试类的输出结果,或许对多态会有更深层次的认识。猜一猜上面
    的结果是什么。

    可以看出来

    子类Cat重写了父类Animal的非静态成员方法am.eat();的输出结果为:猫吃饭。

    子类重写了父类(Animal)的静态成员方法am.sleep();的输出结果为:动物在睡觉

    未被子类(Cat)重写的父类(Animal)方法am.run()输出结果为:动物在奔跑

    那么我们可以根据以上情况总结出多态成员访问的特点:

    成员变量
    - 编译看左边(父类),运行看左边(父类)
    成员方法
    - 编译看左边(父类),运行看右边(子类)。动态绑定
    静态方法
    - 编译看左边(父类),运行看左边(父类)。
    
    (静态和类相关,算不上重写,所以,访问还是左边的)
    只有非静态的成员方法,编译看左边,运行看右边
    

    那么多态有什么弊端呢?

    不能使用子类特有的成员属性和子类特有的成员方法。

    参考:https://www.zhihu.com/question/30082151

    很明显,执行强转语句Cat ct = (Cat)am;之后,ct就指向最开始在堆内存中创建的那个Cat类型的对象了。
    这就是多态的魅力吧,虽然它有缺点,但是它确实十分灵活,减少多余对象的创建,不用说为了使用子类的某个方法又去重新再堆内存中开辟一个新的子类对象。

    数据类型

    String是类类型,不是基本类型。

    基本类型 有八种:整型 (4种)字符型 (1种)浮点型 (2种)布尔型(1种)

    [图片上传失败...(image-5a07c5-1548051041893)]

    [图片上传失败...(image-d3c767-1548051041893)]

    缓存池

    public class Main_1 {
        public static void main(String[] args) {
            Integer a = 1;
            Integer b = 2;
            Integer c = 3;
            Integer d = 3;
            Integer e = 321;
            Integer f = 321;
            Long g = 3L;
            System.out.println(c == d);//true,缓存池
            System.out.println(e == f);//false,不在缓存池
            System.out.println(c == (a + b));//true
            System.out.println(c.equals(a + b));//true
            System.out.println(g == (a + b));//true
            System.out.println(g.equals(a + b));//false
            System.out.println(g.equals(a + h));//true
        }
    

    使用==的情况:

    • 如果比较Integer变量,默认比较的是地址值。
    • 特例:如果比较的某一边有操作表达式(例如a+b),那么比较的是具体数值

    使用equals()的情况:

    • 无论是Integer还是Long中的equals()默认比较的是数值
    • 特例:Long的equals()方法,JDK的默认实现:会判断是否是Long类型

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

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

    valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    

    基本类型对应的缓冲池如下:

    • 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

    String

    概览

    String 被声明为 final,因此它不可被继承。

    在 Java 8 中,String 内部使用 char 数组存储数据。

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    }
    

    在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final byte[] value;
    
        /** The identifier of the encoding used to encode the bytes in {@code value}. */
        private final byte coder;
    }
    

    不可变的好处

    1. 可以缓存 hash 值

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

    2. String Pool 的需要

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

    注意:不是string创建后就默认进入池的,请看下方intern()

    在这里插入图片描述

    3. 安全性

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

    4. 线程安全

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

    Program Creek : Why String is immutable in Java?

    String, StringBuffer and StringBuilder

    1. 可变性

    • String 不可变
    • StringBuffer 和 StringBuilder 可变

    2. 线程安全

    • String 不可变,因此是线程安全的
    • StringBuilder 不是线程安全的
    • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

    对于三者使用的总结:

    1.如果要操作少量的数据用 String

    2.单线程操作字符串缓冲区下操作大量数据 StringBuilder

    3.多线程操作字符串缓冲区下操作大量数据 StringBuffer

    String 和StringBuffer的区别?

    String是immutable的,其内容一旦创建好之后,就不可以发生改变。

    StringBuffer 是可以变长的,内容也可以发生改变
    改变的原理是StringBuffer内部采用了字符数组存放数据,在需要增加长度的时候,创建新的数组,并且把原来的数据复制到新的数组这样的办法来实现。

    https://blog.csdn.net/yeweiyang16/article/details/51755552

    初始化:可以指定给对象的实体的初始容量为参数字符串s的长度额外再加16个字符

    扩容:尝试将新容量扩为大小变成原容量的1倍+2,然后if判断一下 容量如果不够,直接扩充到需要的容量大小。

    StackOverflow : String, StringBuffer, and StringBuilder

    String Pool

    字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。

    当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

    下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。

    String s1 = new String("aaa");
    String s2 = new String("aaa");
    System.out.println(s1 == s2);           // false
    String s3 = s1.intern();
    String s4 = s1.intern();
    System.out.println(s3 == s4);           // true
    

    如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。

    String s5 = "bbb";
    String s6 = "bbb";
    System.out.println(s5 == s6);  // true
    

    在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

    new String("abc")

    使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。

    • "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量;
    • 而使用 new 的方式会在堆中创建一个字符串对象。

    创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。

    public class NewStringTest {
        public static void main(String[] args) {
            String s = new String("abc");
        }
    }
    

    使用 javap -verbose 进行反编译,得到以下内容:

    // ...
    Constant pool:
    // ...
       #2 = Class              #18            // java/lang/String
       #3 = String             #19            // abc
    // ...
      #18 = Utf8               java/lang/String
      #19 = Utf8               abc
    // ...
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=2, args_size=1
             0: new           #2                  // class java/lang/String
             3: dup
             4: ldc           #3                  // String abc
             6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
             9: astore_1
    // ...
    

    在 Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。

    以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    

    运算

    参数传递

    参数传递

    Java 的参数是以值传递的形式传入方法中,而不是引用传递。

    以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。

    public class Dog {
    
        String name;
    
        Dog(String name) {
            this.name = name;
        }
    
        String getName() {
            return this.name;
        }
    
        void setName(String name) {
            this.name = name;
        }
    
        String getObjectAddress() {
            return super.toString();
        }
    }
    
    public class PassByValueExample {
        public static void main(String[] args) {
            Dog dog = new Dog("A");
            System.out.println(dog.getObjectAddress()); // Dog@4554617c
            func(dog);
            System.out.println(dog.getObjectAddress()); // Dog@4554617c
            System.out.println(dog.getName());          // A
        }
    
        private static void func(Dog dog) {
            System.out.println(dog.getObjectAddress()); // Dog@4554617c
            dog = new Dog("B");
            System.out.println(dog.getObjectAddress()); // Dog@74a14482
            System.out.println(dog.getName());          // B
        }
    }
    

    如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。

    class PassByValueExample {
        public static void main(String[] args) {
            Dog dog = new Dog("A");
            func(dog);
            System.out.println(dog.getName());          // B
        }
    
        private static void func(Dog dog) {
            dog.setName("B");
        }
    }
    

    StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?

    float 与 double

    Java 不能隐式执行向下转型,因为这会使得精度降低。

    1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。Java 不能隐式执行向下转型,因为这会使得精度降低。

    // float f = 1.1;
    

    1.1f 字面量才是 float 类型。

    float f = 1.1f;
    

    隐式类型转换

    因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。

    short s1 = 1;
    // s1 = s1 + 1;
    

    但是使用 += 运算符可以执行隐式类型转换。

    s1 += 1;
    

    上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:

    s1 = (short) (s1 + 1);
    

    StackOverflow : Why don't Java's +=, -=, *=, /= compound assignment operators require casting?

    switch

    从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。

    String s = "a";
    switch (s) {
        case "a":
            System.out.println("aaa");
            break;
        case "b":
            System.out.println("bbb");
            break;
    }
    

    switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。

    继承

    访问权限

    Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。

    可以对类或类中的成员(字段以及方法)加上访问修饰符。

    • 类可见表示其它类可以用这个类创建实例对象。
    • 成员可见表示其它类可以用这个类的实例对象访问到该成员;

    protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。

    如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别:这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。

    字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。

    例如下面的例子中,AccessExample 拥有 id 共有字段,如果在某个时刻,我们想要使用 int 去存储 id 字段,那么就需要去修改所有的客户端代码。

    public class AccessExample {
        public String id;
    }
    

    可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。

    public class AccessExample {
    
        private int id;
    
        public String getId() {
            return id + "";
        }
    
        public void setId(String id) {
            this.id = Integer.valueOf(id);
        }
    }
    

    但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。

    public class AccessWithInnerClassExample {
        private class InnerClass {
            int x;
        }
    
        private InnerClass innerClass;
    
        public AccessWithInnerClassExample() {
            innerClass = new InnerClass();
        }
    
        public int getValue() {
            return innerClass.x;  // 直接访问
        }
    }
    

    抽象类与接口

    1. 抽象类

    抽象类和普通类最大的区别是:

    抽象类不能被实例化,需要继承抽象类才能实例化其子类

    public abstract class AbstractClassExample {
    
        protected int x;
        private int y;
    
        public abstract void func1();
    
        public void func2() {
            System.out.println("func2");
        }
    }
    
    public class AbstractExtendClassExample extends AbstractClassExample {
        @Override
        public void func1() {
            System.out.println("func1");
        }
    }
    

    2. 接口

    接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。

    从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。

    在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。

    接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。

    接口的字段默认都是 static 和 final 的。

    public interface InterfaceExample {
        void func1();
    
        default void func2(){
            System.out.println("func2");
        }
    
        int x = 123;
        // int y;               // Variable 'y' might not have been initialized
        public int z = 0;       // Modifier 'public' is redundant for interface fields
        // private int k = 0;   // Modifier 'private' not allowed here
        // protected int l = 0; // Modifier 'protected' not allowed here
        // private void fun3(); // Modifier 'private' not allowed here
    }
    
    public class InterfaceImplementExample implements InterfaceExample {
        @Override
        public void func1() {
            System.out.println("func1");
        }
    }
    

    3. 比较

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

    4. 使用选择

    使用接口:

    • 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
    • 需要使用多重继承。

    使用抽象类:

    • 需要在几个相关的类中共享代码。
    • 需要能控制继承来的成员的访问权限,而不是都为 public。
    • 需要继承非静态static和非常量final字段。

    在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。

    super

    • 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
    • 访问父类的成员:如果子类重写了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。

    Using the Keyword super

    继承相关小问题

    接口是否可继承接口?

    可以,比如List 就继承了接口Collection

    抽象类是否可实现(implements)接口?

    可以,比如 MouseAdapter鼠标监听适配器 是一个抽象类,并且实现了MouseListener接口

    抽象类是否可继承实体类(concrete class)?

    可以,所有抽象类,都继承了Object

    Object 通用方法

    经典:

    https://fangjian0423.github.io/2016/03/12/java-Object-method/

    概览

    
    public native int hashCode()
    
    public boolean equals(Object obj)
    
    protected native Object clone() throws CloneNotSupportedException
    
    public String toString()
    
    public final native Class<?> getClass()
    
    protected void finalize() throws Throwable {}
    
    public final native void notify()
    
    public final native void notifyAll()
    
    public final native void wait(long timeout) throws InterruptedException
    
    public final void wait(long timeout, int nanos) throws InterruptedException
    
    public final void wait() throws InterruptedException
    
    • getClass方法

      • 返回当前运行时对象的Class对象
    • hashCode方法

      • 该方法返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。(文章中有对hashcode的详细解释)
    • equals方法

      • 如果重写了equals方法,通常有必要重写hashCode方法,这点已经在hashCode方法中说明了。
    • clone方法

      • 创建并返回当前对象的一份拷贝。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
      • 使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
    • toString方法

      • Object对象的默认实现,即输出类的名字@实例的哈希码的16进制。
    • notify方法:

      • 唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。
    • notifyAll方法

      • 跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
    • wait(long timeout) throws InterruptedException方法

      • wait方法会让当前线程等待直到另外一个线程调用对象的notify或notifyAll方法,或者超过参数设置的timeout超时时间。
    • wait(long timeout, int nanos) throws InterruptedException方法

      • 跟wait(long timeout)方法类似,多了一个nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
    • wait() throws InterruptedException方法

      • 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。

      需要注意的是 wait(0, 0)和wait(0)效果是一样的,即一直等待。

    • finalize方法

      • 该方法的作用是实例被垃圾回收器回收的时候触发的操作,就好比 “死前的最后一波挣扎”。

    补充:什么是Native Method

    简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。

    equals()

    1. 等价关系

    Ⅰ 自反性

    x.equals(x); // true
    

    Ⅱ 对称性

    x.equals(y) == y.equals(x); // true
    

    Ⅲ 传递性

    if (x.equals(y) && y.equals(z))
        x.equals(z); // true;
    

    Ⅳ 一致性

    多次调用 equals() 方法结果不变

    x.equals(y) == x.equals(y); // true
    

    Ⅴ 与 null 的比较

    对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false

    x.equals(null); // false;
    

    2. 等价与相等

    • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
    • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
    Integer x = new Integer(1);
    Integer y = new Integer(1);
    System.out.println(x.equals(y)); // true
    System.out.println(x == y);      // false
    

    3. 实现

    • 检查是否为同一个对象的引用,如果是直接返回 true;
    • 检查是否是同一个类型,如果不是,直接返回 false;
    • 将 Object 对象进行转型;
    • 判断每个关键域是否相等。
    public class EqualExample {
    
        private int x;
        private int y;
        private int z;
    
        public EqualExample(int x, int y, int z) {
            this.x = x;
            this.y = y;
            this.z = z;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
    
            EqualExample that = (EqualExample) o;
    
            if (x != that.x) return false;
            if (y != that.y) return false;
            return z == that.z;
        }
    }
    

    4. 两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?

    因为hashCode()方法和equals()方法都可以通过自定义类重写,是可以做到equals相同,但是hashCode不同的

    但是,在Object类的equals()方法中有这么一段话

    翻译如下:

    通常来讲,在重写这个方法的时候,也需要对hashCode方法进行重写,
    以此来保证这两个方法的一致性——
    当equals返回true的时候,这两个对象一定有相同的hashcode.

    hashCode()

    hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。

    在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。

    理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。

    一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x,编译器会自动进行这个优化。

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + x;
        result = 31 * result + y;
        result = 31 * result + z;
        return result;
    }
    

    toString()

    默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。

    public class ToStringExample {
    
        private int number;
    
        public ToStringExample(int number) {
            this.number = number;
        }
    }
    
    ToStringExample example = new ToStringExample(123);
    System.out.println(example.toString());
    
    ToStringExample@4554617c
    

    clone()

    1. cloneable

    clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

    public class CloneExample {
        private int a;
        private int b;
    }
    
    CloneExample e1 = new CloneExample();
    // CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
    

    重写 clone() 得到以下实现:

    public class CloneExample {
        private int a;
        private int b;
    
        @Override
        public CloneExample clone() throws CloneNotSupportedException {
            return (CloneExample)super.clone();
        }
    }
    
    CloneExample e1 = new CloneExample();
    try {
        CloneExample e2 = e1.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    
    java.lang.CloneNotSupportedException: CloneExample
    

    以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。

    应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。

    public class CloneExample implements Cloneable {
        private int a;
        private int b;
    
        @Override
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    

    2. 浅拷贝

    拷贝对象和原始对象的引用类型引用同一个对象。

    public class ShallowCloneExample implements Cloneable {
    
        private int[] arr;
    
        public ShallowCloneExample() {
            arr = new int[10];
            for (int i = 0; i < arr.length; i++) {
                arr[i] = i;
            }
        }
    
        public void set(int index, int value) {
            arr[index] = value;
        }
    
        public int get(int index) {
            return arr[index];
        }
    
        @Override
        protected ShallowCloneExample clone() throws CloneNotSupportedException {
            return (ShallowCloneExample) super.clone();
        }
    }
    
    ShallowCloneExample e1 = new ShallowCloneExample();
    ShallowCloneExample e2 = null;
    try {
        e2 = e1.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    e1.set(2, 222);
    System.out.println(e2.get(2)); // 222
    

    3. 深拷贝

    拷贝对象和原始对象的引用类型引用不同对象。

    public class DeepCloneExample implements Cloneable {
    
        private int[] arr;
    
        public DeepCloneExample() {
            arr = new int[10];
            for (int i = 0; i < arr.length; i++) {
                arr[i] = i;
            }
        }
    
        public void set(int index, int value) {
            arr[index] = value;
        }
    
        public int get(int index) {
            return arr[index];
        }
    
        @Override
        protected DeepCloneExample clone() throws CloneNotSupportedException {
            DeepCloneExample result = (DeepCloneExample) super.clone();
            result.arr = new int[arr.length];
            for (int i = 0; i < arr.length; i++) {
                result.arr[i] = arr[i];
            }
            return result;
        }
    }
    
    DeepCloneExample e1 = new DeepCloneExample();
    DeepCloneExample e2 = null;
    try {
        e2 = e1.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    e1.set(2, 222);
    System.out.println(e2.get(2)); // 2
    

    4. clone() 的替代方案

    使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

    public class CloneConstructorExample {
    
        private int[] arr;
    
        public CloneConstructorExample() {
            arr = new int[10];
            for (int i = 0; i < arr.length; i++) {
                arr[i] = i;
            }
        }
    
        public CloneConstructorExample(CloneConstructorExample original) {
            arr = new int[original.arr.length];
            for (int i = 0; i < original.arr.length; i++) {
                arr[i] = original.arr[i];
            }
        }
    
        public void set(int index, int value) {
            arr[index] = value;
        }
    
        public int get(int index) {
            return arr[index];
        }
    }
    
    CloneConstructorExample e1 = new CloneConstructorExample();
    CloneConstructorExample e2 = new CloneConstructorExample(e1);
    e1.set(2, 222);
    System.out.println(e2.get(2)); // 2
    

    关键字

    final

    • 修饰类:表示该类不能被继承

    • 修饰方法:表示该方法不能被重写

    • 修饰变量:表示该变量只能被赋值一次

      声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。

      • 对于基本类型,final 使数值不变;
      • 对于引用类型,final 使引用不变,表示该引用只有一次指向对象的机会,也就不能引用其它对象,但是被引用的对象本身是可以修改的。

    finally

    finally 是用于异常处理的场面,无论是否有异常抛出,都会执行

    finalize

    finalize是Object的方法,所有类都继承了该方法。 当一个对象满足垃圾回收的条件,并且被回收的时候,其finalize()方法就会被调用

    static

    1. 静态变量

    • 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它;静态变量在内存中只存在一份。
    • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
    public class A {
        private int x;         // 实例变量
        private static int y;  // 静态变量
    
        public static void main(String[] args) {
            // int x = A.x;  // Non-static field 'x' cannot be referenced from a static context
            A a = new A();
            int x = a.x;
            int y = A.y;
        }
    }
    

    2. 静态方法

    静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。

    public abstract class A {
        public static void func1(){
        }
        // public abstract static void func2();  // Illegal combination of modifiers: 'abstract' and 'static'
    }
    

    只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。

    public class A {
        private static int x;
        private int y;
    
        public static void func1(){
            int a = x;
            // int b = y;  // Non-static field 'y' cannot be referenced from a static context
            // int b = this.y;     // 'A.this' cannot be referenced from a static context
        }
    }
    

    3. 静态语句块

    静态语句块在类初始化时运行一次。

    public class A {
        static {
            System.out.println("123");
        }
    
        public static void main(String[] args) {
            A a1 = new A();
            A a2 = new A();
        }
    }
    
    123
    

    4. 静态内部类

    非静态内部类依赖于外部类的实例,而静态内部类不需要。

    当一个内部类没有使用static修饰的时候,是不能直接使用内部类创建对象,须要先使用外部类对象.new内部类对象及(外部类对象.new 内部类())

    而静态内部类只需要new OuterClass.InnerClass();

    public class OuterClass {
        class InnerClass {
        }
    
        static class StaticInnerClass {
        }
    
        public static void main(String[] args) {
            // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
            OuterClass outerClass = new OuterClass();
            InnerClass innerClass = outerClass.new InnerClass();
            StaticInnerClass staticInnerClass = new StaticInnerClass();
        }
    }
    

    静态内部类不能访问外部类的非静态的变量和方法。

    5. 静态导包

    在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

    import static com.xxx.ClassName.*
    

    6. 初始化顺序

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

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

    反射

    每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。

    类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。

    反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。

    Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:

    • Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
    • Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
    • Constructor :可以用 Constructor 创建新的对象。

    反射的优点:

    • 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
    • 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
    • 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。

    反射的缺点:

    尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。

    • 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。

    • 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。

    • 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

    异常

    运行时异常:

    NullPointerException 空指针异常
    ArithmeticException 算术异常,比如除数为零
    ClassCastException 类型转换异常
    ConcurrentModificationException同步修改异常,遍历一个集合的时候,删除集合的元素,就会抛出该异常 
    IndexOutOfBoundsException 数组下标越界异常
    NegativeArraySizeException 为数组分配的空间是负数异常
    

    一般异常又叫做可查异常(受检异常),在编译过程中,必须进行处理,要么捕捉,要么通过throws 抛出去.

    比如FileNotFoundException
    
    在这里插入图片描述

    Error和Exception有什么区别?

    Error和Exception都实现了Throwable接口

    Error指的是JVM层面的错误,比如内存不足OutOfMemoryError

    Exception 指的是代码逻辑的异常,比如下标越界OutOfIndexException

    泛型

    public class Box<T> {
        // T stands for "Type"
        private T t;
        public void set(T t) { this.t = t; }
        public T get() { return t; }
    }
    

    零散知识点

    &和&&的区别

    & 有两个作用,分别是 位与 和 逻辑与

    && 就是逻辑与

    长路与&:两侧,都会被运算

    短路与&&:只要第一个是false,第二个就不进行运算了

    heap和stack有什么区别

    heap: 堆

    stack: 栈

    • 存放的内容不一样:

      • heap: 是存放对象的
      • stack: 是存放基本类型(int, float, boolean 等等)、引用(对象地址)、方法调用
    • 存取方式不一样:

      • heap: 是自动增加大小的,所以不需要指定大小,但是存取相对较慢
      • stack: 先入后出的顺序,并且存取速度比较快

    获取长度

    数组获取长度的手段是 .length 属性

    String获取长度的手段是 length()方法

    集合获取长度的手段是 size()方法

    文件获取长度的手段是 length()方法

    Set里的元素是不能重复的,那么用什么方法来区分重复与否呢

    以HashSet为例,判断重复的逻辑是:

    1. 首先看hashcode是否相同,如果不同,就是不重复的
    2. 如果hashcode一样,再比较equals,如果不同,就是不重复的,否则就是重复的。

    http://how2j.cn/k/collection/collection-hashcode/371.html#step2530

    构造函数器/Constructor是否可被override?是否可以继承String类?

    子类不能继承父类的构造方法,所以就不存在重写父类的构造方法。

    String是final修饰的,所以不能够被继承

    try catch finally 执行顺序

    try里的return和finally里的return 都会支持,但是当前方法只会采纳finally中return的值

    https://www.cnblogs.com/superFish2016/p/6687549.html

    总结以上测试:

    1、finally语句总会执行

    2、如果try、catch中有return语句,finally中没有return,那么在finally中去修改除了包装类型和静态变量、全局变量以外的数据都不会对try、catch中返回的变量有任何的影响(包装类型、静态变量会改变、全局变量)。但是修改包装类型和静态变量、全局变量,会改变变量的值。

    3、尽量不要在finally中使用return语句,如果使用的话,会忽略try、catch中的返回语句,也会忽略try、catch中的异常,屏蔽了错误的发生。

    4、finally中避免再次抛出异常,一旦finally中发生异常,代码执行将会抛出finally中的异常信息,try、catch中的异常将被忽略。

    所以在实际项目中,finally常常是用来关闭流或者数据库资源的,并不额外做其他操作。

    char型变量中能不能存贮一个中文汉字?为什么?

    char是16位的,占两个字节

    汉字通常使用GBK或者UNICODE编码,也是使用两个字节

    所以可以存放汉字

    一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制?

    可以包括多个类,但是只能出现一个public修饰的类,但是可以出现多个非public修饰的类。

    java中有几种类型的流?

    Java中所有的流都是基于字节流,所以最基本的流是

    输入输出字节流
    
    InputStream
    
    OutputStream
    
    在字节流的基础上,封装了字符流
    
    Reader
    
    Writer
    
    进一步,又封装了缓存流
    
    BufferedReader
    
    PrintWriter
    
    以及数据流
    
    DataInputStream
    
    DataOutputStream
    
    对象流
    
    ObjectInputStream
    
    ObjectOutputStream
    
    以及一些其他的奇奇怪怪的流 ~~~
    

    什么是java序列化,如何实现java序列化?

    序列化指的是把一个Java对象,通过某种介质进行传输,比如Socket输入输出流,或者保存在一个文件里。

    实现java序列化的手段是让该类实现接口 Serializable,这个接口是一个标识性接口,没有任何方法,仅仅用于表示该类可以序列化。

    JAVA序列化ID问题

    https://blog.csdn.net/qq_35370263/article/details/79482993

    虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清单 1 中,虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。

    在JAVA中,如何跳出当前的多重嵌套循环?

    public class HelloWorld {
        public static void main(String[] args) {
               
            //打印单数    
            outloop: //outloop这个标示是可以自定义的比如outloop1,ol2,out5
            for (int i = 0; i < 10; i++) {
                  
                for (int j = 0; j < 10; j++) {
                    System.out.println(i+":"+j);
                    if(0==j%2) 
                        break outloop; //如果是双数,结束外部循环
                }
                  
            }
              
        }
    }
    

    Anonymous Inner Class (匿名内部类) 是否可以extends(继承)其它类,是否可以 implements(实现)interface(接口)?

    匿名内部类本质上就是在继承其他类,实现其他接口

    如例:
    匿名类1,就是继承了Thread
    匿名类2 ,就是实现了Runnable接口

    package j2se;
     
    public class HelloWorld {
     
        public static void main(String[] args) {
     
            // 匿名类1
            new Thread() {
                public void run() {
     
                }
            };
     
            // 匿名类2
            new Runnable() {
                public void run() {
     
                }
            };
     
        }
    }
    

    内部类可以引用外部类的成员吗?有没有什么限制?

    可以使用

    如果是非静态内部类,可是使用外部类的所有成员

    如果是静态内部类,只能使用外部类的静态成员

    Class.forName的作用?为什么要用?

    Class.forName常见的场景是在数据库驱动初始化的时候调用。

    Class.forName本身的意义是加载类到JVM中。 一旦一个类被加载到JVM中,它的静态属性就会被初始化,在初始化的过程中就会执行相关代码,从而达到"加载驱动的效果"

    JDK 中常用的包有哪些?

    答:java.lang、java.util、java.io、java.net、java.sql。

    集合

    在这里插入图片描述

    容器主要包括 Collection 和 Map 两种,Collection 又包含了 List、Set 以及 Queue。

    Collection

    Set

    • HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的;

    • TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN);

    • LinkedHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序。

    List

    • ArrayList:基于动态数组实现,支持随机访问;

    • Vector:和 ArrayList 类似,但它是线程安全的;

    • LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。

    Queue

    • LinkedList:可以用它来支持双向队列;

    • PriorityQueue:基于堆结构实现,可以用它来实现优先队列。

    优先队列是一种用来维护一组元素构成的结合S的数据结构,其中每个元素都有一个关键字key,元素之间的比较都是通过key来比较的。优先队列包括最大优先队列和最小优先队列,优先队列的应用比较广泛,比如作业系统中的调度程序,当一个作业完成后,需要在所有等待调度的作业中选择一个优先级最高的作业来执行,并且也可以添加一个新的作业到作业的优先队列中。Java中,PriorityQueue的底层数据结构就是堆(默认是小堆)。

    Map

    • HashMap:基于哈希实现;

    • HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。

    • LinkedHashMap:使用链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。

    • TreeMap:基于红黑树实现。

    Collection 和 Collections的区别

    Collection是接口,是List和Set的父接口

    Collections是工具类,提供了排序,混淆等等很多实用方法

    List、Set 和 Map 的初始容量和加载因子

    答:
    加载因子的系数小于等于1,意指 即当 元素个数 超过 容量长度*加载因子的系数 时,进行扩容。

    1. List

    ArrayList 的初始容量是 10;加载因子为 0.5; 扩容增量:原容量的 0.5 倍 +1;一次扩容后长度为 16。

    Vector 初始容量为 10,加载因子是 1。扩容增量:原容量的 1 倍,如 Vector 的容量为 10,一次扩容后是容量为 20。

    1. Set

    HashSet,初始容量为 16,加载因子为 0.75; 扩容增量:原容量的 1 倍; 如 HashSet 的容量为 16,一次扩容后容量为 32

    1. Map

    HashMap,初始容量 16,加载因子为 0.75; 扩容增量:原容量的 1 倍; 如 HashMap 的容量为 16,一次扩容后容量为 32

    Comparable 接口和 Comparator 接口有什么区别?

    答:
    详细可以看:https://blog.csdn.net/u011240877/article/details/53399019

    • 对于一些普通的数据类型(比如 String, Integer, Double…),它们默认实现了Comparable 接口,实现了 compareTo 方法,我们可以直接使用。

    • 而对于一些自定义类,它们可能在不同情况下需要实现不同的比较策略,我们可以新创建 Comparator 接口,然后使用特定的 Comparator 实现进行比较。

    对比:

    • Comparable简单,但是如果需要重新定义比较类型时,需要修改源代码。

    • Comparator不需要修改源代码,自定义一个比较器,实现自定义的比较方法。

    Java 集合的快速失败机制 “fail-fast”

    答:

    它是 java 集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

    可以知道在进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,调用相关方法使集合的个数发生改变,就会使modCount发生变化

    每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

    解决办法:

    1. 在遍历过程中,所有涉及到改变 modCount 值得地方全部加上 synchronized;

    2. 使用 CopyOnWriteArrayList 来替换 ArrayList。

    java.util.concurrent包中包含的并发集合类如下:

    详细:http://raychase.iteye.com/blog/1998965

    ConcurrentHashMap
    
    CopyOnWriteArrayList
    
    CopyOnWriteArraySet
    

    相关文章

      网友评论

        本文标题:Java基础知识点面试手册(上)

        本文链接:https://www.haomeiwen.com/subject/xpdcjqtx.html