zoukankan      html  css  js  c++  java
  • 总结一些 Java 相关笔试、面试题,万一用上了呢 (=_=) -- 基础篇

    一、常见概念(可跳过)

    1、JVM、JRE、JDK 关系与区别?

    (1)JVM:
      指的是 Java 虚拟机,加载编译好的 字节码(.class)文件,将其转为机器语言执行。

      对于不同的系统,有不同的 JVM,Java 代码编译一次后,可以在不同系统上的 JVM 上执行,故 Java 代码可以一次编译、到处运行。

    (2)JRE:
      指的是 Java 最小的运行环境,包括 JVM 以及 Java 的系统类库。

    (3)JDK:
      指的是 Java 最小的开发环境,包括 JRE 以及 编译、运行等开发工具。

    2、String 赋值问题?

    (1)直接赋值。即 String str = "hello";
      对于直接赋值,值存在于 常量池中,若常量池存在 “hello”,则 str 指向这个字符串,若不存在,则创建一个 “hello” 置于常量池中,并将其引用返回。

    (2)通过 new 关键字赋值。即 String str = new String("hello");
      对于 new 实例化,如果常量池不存在 “hello”,会创建一个 “hello” 置于常量池中,然后 new 关键字会在 堆 中创建一个 String 对象,并将堆中的引用返回。
      即 new 实例化一个 String 对象时,会创建 一个 或 两个 对象。

    3、== 和 equals 的区别?

    (1)== 用于比较两个数的值是否相同。
      对于 基本类型(char、boolean、byte、short、int、long、float、double), == 比较的是 值是否相同。
      对于 引用类型(除基本类型外的类型),比较的是引用是否相同,即比较 指向引用变量的地址是否相同。

    (2)equals 未重写时,等价于 ==。
      一般重写后用于比较两个对象的值是否相同(比如 String)。

    【举例:】
    public class Main {
    
        public static void main(String[] args) {
            String str1 = "hello";
            String str2 = "hello";
            String str3 = new String("hello");
            String str4 = new String("hello");
            System.out.println(1 == 1); // true,基本类型 直接 比较值,值相同,故为 true
            System.out.println(str1 == str2); // true,String 直接赋值,str1、str2指向常量池同一值,即引用地址相同,故为 true
            System.out.println(str1 == str3); // false,str3 为 new 实例化,在堆中创建,引用地址与 str1 不同,故为 false
            System.out.println(str1.equals(str3)); // true,String 重写 equals 方法,比较的是值,值相同,故为 true
            System.out.println(str3 == str4); // false,str3、str4 均为 new 实例化,两者引用地址不同,故为 false
            System.out.println(str3.equals(str4)); // true,比较的是值,值相同,故为 true
        }
    }

    4、hashCode() 与 equals() 的关系?

    (1) 作用:
      hashCode() 方法用于获取 哈希码(散列值),其返回一个 int 值,用于确定对象在哈希表中的索引位置。
      equals() 方法用于判断两个值(对象)是否相同。

    (2)为什么需要 hashCode() ?以 HashSet 检查重复元素为例。
      当加入一个 对象 到 HashSet 中时,HashSet 首先会计算出 对象的 HashCode 值,并根据该值定位到 对象加入的位置,若不存在相同的 hashCode 值,直接添加对象即可。若此时 存在相同 的 hashCode,则通过 equals() 去比较两个 对象是否 真的相同,如果相同,那么 HashSet 此次加入操作失败,如果不同,则会将该数据散列到 其他位置。
      引入了 hashCode(),可以直接定位到 需要比较的位置,从而减少了 没必要的 equals 比较次数,提高执行速度。

    (3)hashCode() 与 equals() 关系
      equals() 相同的两个对象,hashCode() 必定相同。
      hashCode() 相同的两个对象,equals() 不一定相同。
      equals() 重写时,hashCode() 一般也要重写。
    注:
      hashCode() 默认行为是对 堆中对象 产生独特值,如果没有重写 hashCode(),那么即使这两个对象指向相同的数据,这两个对象也不会相等。

    5、String、StringBuilder、StringBuffer 的区别?

      常用方法详见:https://www.cnblogs.com/l-y-h/p/10910596.html

      通过 JVM 研究一下 Sring:https://www.cnblogs.com/l-y-h/p/13554451.html#_label1

    (1)String 是 final 修改的不可变类,
      内部采用 final 修饰的 char 数组来保存字符串,故String 对象不可变。
      是线程安全的。频繁修改字符串时,使用 String 会频繁的创建对象,会影响效率。故适合操作少量数据。

    (2)StringBuilder 与 StringBuffer 内部直接采用 char 数组来保存字符串,
      适用于频繁修改字符串,且不会产生新的对象。
      StringBuilder 没有使用同步锁,故线程不安全,适用于单线程下操作大量数据。
      StringBuffer 使用同步锁,故线程安全,适合于多线程下操作大量数据。

    6、throw、throws 的区别?

    (1)throw 用于方法内部,后跟异常对象,只能指明一个异常对象。
    (2)throws 用于方法声明上,后跟异常类型,可以一次指明多个异常类型。

    【举例:】
    public class Main {
    
        public static void main(String[] args) throws Exception {
            try {
                System.out.println(10 / 0);
            } catch (ArithmeticException e) {
                throw new Exception();
            }
        }
    }

    7、final、finally、finalize 的区别?

    (1)final 是修饰符,如果修饰类,则此类不能被继承,修饰方法、变量,则表示此方法、变量不能被修改。

    (2)finally 是 try - catch - finally 代码块的最后一部分,可以省略,如果存在,则表示一定会执行(除了特殊原因退出外,比如 System.exit(0))。

    (3)finalize 是 Object 的一个方法,GC 执行时会自动调用被回收对象的该方法(执行时机不可控)。

    8、try - catch - finally 的使用?

    (1)try - catch - finally 块中 catch、finally 均可以省略,但是必须存在一个,即 try - catch 或者 try - finally 必须存在一个。

    (2)对于 try - catch - finally 中出现 return 的情况时,若 finally 中无 return,则返回 try 或 catch 中return 的结果。若 finally 中有 return,则返回 finally 中 return 的结果。

    public class Main {
    
        public static void main(String[] args) {
            System.out.println(test()); // 输出 1,finally 中无 return,返回 try 中 return 的结果
            System.out.println(test2()); // 输出 4,finally 中有 return,返回 finally 中 return 的结果
        }
    
        public static int test() {
            int a = 1;
            try {
                return a;
            }finally {
                a = 4;
            }
        }
    
        public static int test2() {
            int a = 1;
            try {
                return a;
            }finally {
                a = 4;
                return a;
            }
        }
    }

    9、Java 面向对象编程三大特性

    (1)三大特性:
      封装、继承、多态。

    (2)封装:
      封装 指的是 将客观的事物 包装成抽象的类,并封装其代码逻辑、通过访问权限控制符 来控制访问方式,进而保护程序。
      比如:将类 的成员变量 私有化,并提供给 外界 一个 访问、修改 该变量的方法。如果 不想该成员变量 被外界访问,那么可以 不对外提供 访问、修改 自身的方法。
    注:
      如果一个类 没有提供给外界 访问自身的方法,那么这个类一般没什么实际意义。
      使用反射时,可以获取到 对象 未对外提供的 私有方法以及成员,此时会破坏封装性。

    (3)继承:
      继承 指的是 以某个类为基础 创建新的类(代码复用)。基础类 称为 父类,新的类 称为 子类。
      子类 通过 继承父类的方式,可以获取到 父类所有的属性、方法(包括私有属性、方法,但是子类只是拥有,无法修改)。子类可以 编写自己的方法、扩展程序。
    注:
      对于 父类 非 private 修饰的 方法、属性,子类可以直接访问、修改。
      对于 父类 private 修饰的 方法、属性,子类只是拥有、不能直接修改,可以通过 父类对外暴露 的 方法进行 访问、修改。

    (4)多态:
      多态 指的是 一个类的实例 的相同方法 在不同情形下有不同的结果,一般在 程序运行期 才能确定。也即 一个 引用变量 其具体的 引用类型 以及 通过 该引用变量 真实调用的 方法 只有在 程序运行期 才能确定。

    多态常见形式:
      继承。多个子类 继承 一个父类,并对 父类中的 同一个方法 进行方法重写。此时使用 父类 去声明一个子类时,需要在程序运行期 才能知道具体是哪个子类。
      接口。实现接口 并 覆盖接口中的 抽象方法,那么也需要在 程序运行期 才能确定 真实调用的方法。

    10、接口 与 抽象类的区别?

    (1)接口:
      Java 8 之前,接口中 方法默认修饰符为 public abstract(可省略),且没有方法体(即方法不能有默认实现)。Java8 开始后,接口中 方法可以有默认实现(使用 default 去修饰方法)。
      接口中成员变量 默认修饰符为 public final(即 常量)。
      一个类可以实现多个接口,但是只能实现一个抽象类。
      一个类实现 接口 时,必须重写该接口的所有抽象方法(default 方法可以不重写)。
      接口不能直接通过 new 去实例化,但是可以通过 new 实例化 实现该接口 的子类。

    (2)抽象类:
      抽象类中可以存在 非抽象方法,并定义方法体(默认实现)。
      抽象类中可以存在 非 final 修饰的成员变量。
      一个类继承 抽象类 时,必须重写该抽象类中所有抽象方法(非抽象方法可以不重写)。

    (3)使用场景:
      抽象是对 类 的抽象,一般存储 某类事物共有的属性、方法。属于 模板设计。
      接口是对 行为 的抽象,一般存储 某类事物 特有的方法。属于 行为规范。

    11、常用获取键盘输入的方式

    【方式一:通过 Scanner】
        Scanner scanner = new Scanner(System.in);
        // 获取一行数据
        String str = scanner.nextLine();
        // 获取下一个整型数据
        int age = scanner.nextInt();
        System.out.println(str);
        System.out.println(age);
        scanner.close();
    
    【方式二:通过 BufferedReader】
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        try {
            // 获取一行数据
            String str = bufferedReader.readLine();
            System.out.println(str);
            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    二、易混概念(可以瞅两眼)

    1、自增变量赋值问题

    (1)问题:
      下面代码输出的值为多少?(主要涉及 变量如何变化、赋值等问题)

    public class Main {
        public static void main(String[] args) {
            int i = 1;
            i = I++;
            int j = I++;
            int k = i + ++i * I++;
            System.out.println("i = " + i);
            System.out.println("j = " + j);
            System.out.println("k = " + k);
        }
    }

    (2)回答:
      输出如下:

    i = 4
    j = 1
    k = 11

    分析:
      可以结合着下面的字节码文件一起看,主要就是 局部变量表 以及 操作数栈 的值的变化。
      此处简单分析一下过程。

        此时 i = 1。
        局部变量保存一个 i = 1
    
    【i = i++;】
        此时 i = 1。
        对于 i++ ,会先把 i 的值压入操作数栈,即操作数栈保存 i = 1,i++ 使 局部变量中为 i = 2,
        然后 赋值操作(=)会将 操作数栈的值赋给局部变量 i,即最后局部变量中保存的为 i = 1。
    注:
        i++    可理解为先赋值(压入操作数栈),再 ++(局部变量++)
        ++i    可理解为先 ++,再赋值
    
    【int j = i++;】
        此时 j = 1, i = 2。
        与 i = i++ 类似,只是此时,赋值操作将值压入 局部变量 j 中,所以 j = 1, i = 2。
    
    【int k = i + ++i * i++;】
        此时 i = 4, k = 11。
        按顺序压入操作数栈,可以分为 i、++i、i++ 三步(操作数栈有三个值),
        首先局部变量 i 为 2,操作数栈为 2,
        ++i 使局部变量 i = 3, 操作数栈为 3(先 ++,再压栈),
        i++ 使局部变量 i = 4, 操作数栈为 3(先压栈,再 ++).
        由于 * 优先级高于 +,所以 ++i * i++ 会先执行,即 3 * 3 = 9.
        最后执行 +,即 9 + 2 = 11,然后将该值赋给 k 变量。
        所以,最后局部变量 i = 4,k = 11

    (3)查看字节码
      idea 中使用 bytecode viewer 插件可用于查看字节码文件。

    通过 菜单栏的 view 中的 show bytecode,可以查看字节码文件。

    上例代码转为字节码文件如下:(看不懂的,就直接跳过吧)

    // class version 52.0 (52)
    // access flags 0x21
    public class Main {
    
      // compiled from: Main.java
    
      // access flags 0x1
      public <init>()V
       L0
        LINENUMBER 1 L0
        ALOAD 0
        INVOKESPECIAL java/lang/Object.<init> ()V
        RETURN
       L1
        LOCALVARIABLE this LMain; L0 L1 0
        MAXSTACK = 1
        MAXLOCALS = 1
    
      // access flags 0x9
      public static main([Ljava/lang/String;)V
       L0
        LINENUMBER 3 L0
        ICONST_1
        ISTORE 1
       L1
        LINENUMBER 4 L1
        ILOAD 1
        IINC 1 1
        ISTORE 1
       L2
        LINENUMBER 5 L2
        ILOAD 1
        IINC 1 1
        ISTORE 2
       L3
        LINENUMBER 6 L3
        ILOAD 1
        IINC 1 1
        ILOAD 1
        ILOAD 1
        IINC 1 1
        IMUL
        IADD
        ISTORE 3
       L4
        LINENUMBER 7 L4
        GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
        NEW java/lang/StringBuilder
        DUP
        INVOKESPECIAL java/lang/StringBuilder.<init> ()V
        LDC "i = "
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        ILOAD 1
        INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
        INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
        INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
       L5
        LINENUMBER 8 L5
        GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
        NEW java/lang/StringBuilder
        DUP
        INVOKESPECIAL java/lang/StringBuilder.<init> ()V
        LDC "j = "
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        ILOAD 2
        INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
        INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
        INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
       L6
        LINENUMBER 9 L6
        GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
        NEW java/lang/StringBuilder
        DUP
        INVOKESPECIAL java/lang/StringBuilder.<init> ()V
        LDC "k = "
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        ILOAD 3
        INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
        INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
        INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
       L7
        LINENUMBER 10 L7
        RETURN
       L8
        LOCALVARIABLE args [Ljava/lang/String; L0 L8 0
        LOCALVARIABLE i I L1 L8 1
        LOCALVARIABLE j I L3 L8 2
        LOCALVARIABLE k I L4 L8 3
        MAXSTACK = 3
        MAXLOCALS = 4
    }

    (4)总结
      对于赋值操作(值 = 表达式),基本顺序如下:
        对于 = 右边表达式,从左到右按顺序执行,依次压入操作数栈,然后根据表达式的优先级进行相关计算。
        表达式中若出现自增、自减操作(++、--),会直接操作局部变量,不经过操作数栈。
        赋值操作(=),是最后执行的,其会将操作数栈的最终的值返回。

    2、类初始化、实例化过程 以及 方法重写等问题

    (1)问题:
      下面代码输出值为多少?(主要涉及 类初始化、实例化过程 以及 方法重写等问题)

    public class Main {
        public static void main(String[] args) {
            Son son1 = new Son();
            System.out.println();
            Son son2 = new Son();
            System.out.println();
            Son son3 = new Son(1);
        }
    }
    class Father {
        private int i = test();
        private static int j = test2();
        private final int K = test3();
    
        Father() {
            System.out.print("(1)");
        }
    
        static {
            System.out.print("(2)");
        }
    
        {
            System.out.print("(3)");
        }
    
        public int test() {
            System.out.print("(4)");
            return 1;
        }
    
        public static int test2() {
            System.out.print("(5)");
            return 1;
        }
    
        private int test3() {
            System.out.print("(6)");
            return 1;
        }
    }
    
    class Son extends Father{
        private int i = test();
        private static int j = test2();
        private final int K = test3();
    
        Son() {
            System.out.print("(7)");
        }
    
        Son(int i) {
            System.out.print("(8)");
        }
    
        static {
            System.out.print("(9)");
        }
    
        {
            System.out.print("(10)");
        }
    
        public int test() {
            System.out.print("(11)");
            return 1;
        }
    
        public static int test2() {
            System.out.print("(12)");
            return 1;
        }
    
        private int test3() {
            System.out.print("(13)");
            return 1;
        }
    }

    (2)回答:
      输出如下:

    (5)(2)(12)(9)(11)(6)(3)(1)(11)(13)(10)(7)
    (11)(6)(3)(1)(11)(13)(10)(7)
    (11)(6)(3)(1)(11)(13)(10)(8)

    首先得对类初始化、实例化、方法重写有个基本概念。


    类初始化过程:
      首先会找到 main 方法所在的类,然后加载、初始化。
      加载时,若该类存在父类,则会先加载、初始化父类。
      通过字节码文件(使用 bytecode viewer 插件可以查看,此处不再重复截图)可以看到 类初始化时,会执行 <clinit>() 相关操作。
    clinit 主要加载的为 静态代码块 和 静态代码变量,且执行顺序为从上到下执行,只会执行一次(即只加载一次静态变量、静态代码块)。

    实例化过程:
      通过字节码文件,可以看到实例化过程,会执行 <init>() 相关操作,有几个构造器,就有几个 <init>()。当然,执行哪个构造器,才会执行那个 init 操作。
      init 主要加载的为 非静态代码块、非静态变量、构造器,且执行顺序为从上到下执行,但构造器最后执行。当然,若有父类,则会先加载父类(执行父类的 init 操作)。

    方法重写(Override):
      使用 final、static 关键字修饰的方法不可被重写。
      使用 private 修饰的方法,子类也不可重写该方法。
      非静态方法默认调用对象为 this,即当前正在创建的对象,所以如果子类重写了父类的方法,由于正在创建子类,执行的是子类的方法,而非父类的方法。

    (3)执行结果分析
      通过上面的基本概念,应该不难得到最后的输出结果。
      对于 Son son1 = new Son();
    首先,执行类加载过程。
      加载 main 方法所在的类 Son,因为其继承 Father,所以先加载 Father、再加载 Son 的静态变量、静态代码块。即输出:(5)(2)(12)(9)。

    然后,执行实例化过程。
      先加载 Father,再加载 Son。由于 父类的 test 被 子类的 test 方法覆盖(重写),而 test3 被 final 修饰,不会被子类的 test3 覆盖,构造器最后加载,所以输出结果为:(11)(6)(3)(1)(11)(13)(10)(7)。

      对于 Son son2 = new Son();
    由于之前已经执行过一次 类加载过程,所以此处不会再执行,即输出结果为 (11)(6)(3)(1)(11)(13)(10)(7)。

      对于 Son son3 = new Son(1);
    由于指定了不同的构造器,所以会执行不同的 init 操作,即输出结果为 (11)(6)(3)(1)(11)(13)(10)(8)。

    (4)总结
    一般加载过程:
      父类的静态变量 -》 父类的静态代码块 -》 子类的静态变量 -》 子类的静态代码块 -》父类的非静态变量 -》 父类非静态代码块 -》 父类的构造方法 -》 子类的非静态变量 -》 子类的非静态代码块 -》 子类的构造方法。
      对于重写方法(非 final、static、private 修饰的父类方法),会执行子类的方法。

    3、方法参数传递问题

    (1)问题:
      下面代码输出值为多少?(主要涉及 参数传参的值传递、引用传递问题)

    import java.util.Arrays;
    
    public class Main {
        public static void main(String[] args) {
            int i = 1;
            Integer num = 2;
            Integer num2 = 200;
            String string = "hello";
            int[] arrays = new int[]{1, 2, 3, 4, 5};
            int[] arrays2 = new int[]{1, 2, 3, 4, 5};
            Test test = new Test();
    
            changeValue(i, num, num2, string, arrays, arrays2, test);
    
            System.out.println("i = " + i);
            System.out.println("num = " + num);
            System.out.println("num2 = " + num2);
            System.out.println("string = " + string);
            System.out.println("arrays = " + Arrays.toString(arrays));
            System.out.println("arrays2 = " + Arrays.toString(arrays2));
            System.out.println("test = " + test.a);
        }
    
        public static void changeValue(int i, Integer num, Integer num2, String string, int[] arrays, int[] arrays2, Test test) {
            I++;
            num++;
            num2++;
            string += " world";
            arrays[2]++;
            arrays2 = new int[]{1, 2};
            test.a++;
        }
    }
    class Test {
        public int a = 10;
    }

    (2)回答;
      输出值如下:

    i = 1
    num = 2
    num2 = 200
    string = hello
    arrays = [1, 2, 4, 4, 5]
    arrays2 = [1, 2, 3, 4, 5]
    test = 11

    首先对值传递、引用传递有个基本认识:
      值传递:传递数据值。
      引用传递:传递地址值(数据所在的地址)。

    形参、实参:
      形参就是 方法中的 参数。
      实参就是 实际要传递给 方法的 参数。

    值传递时,形参的变化不会影响实参中的数据。
    引用传递时,由于传递的是地址,若修改地址,则不会影响实参的数据,但若是通过地址修改数据,则会影响实参的数据。

    在 Java 中,
      对于基本数据类型,其使用 值传递,即实参向形参中传递的为 数据值。
      对于引用数据类型,其使用 引用传递,即实参向形参中传递的为 数据所在的地址值。
      当然,对于 String、以及 Integer 等包装类,由于其值的不可变性,其值变化时,会重新指向另一个变量(即地址发生改变)。

    (3)分析:

    【int i = 1;】
        由于 int 属于基本数据类型,其传递的是数据值,形参的变化不影响。
        即 i++ 不会影响原数据,输出 i = 1.
    
    【Integer num = 2;】
        Integer 属于包装类,但由于其不可变性,其值变化时,会重新指向另一个对象。
        也即地址改变,所以不会影响原数据,输出 num = 2.
    注:
        Integer 内部 缓存了 -128 ~ 127 的值。
        即使用 Integer 保存了 -128 ~ 127 的值时,其不会重新 new 个对象,为同一个对象。
        Integer test1 = 1;
        Integer test2 = 1;
        System.out.println(test1 == test2);  // 输出结果为 true
    
    【Integer num2 = 200;】
        同上处理,输出 num2 = 200。
    
    【String string = "hello";】
        String 类型也存在不可变性,与包装类类似,修改值修改的是地址,不会影响原数据。
        所以输出值为 string = hello
    
    【int[] arrays = new int[]{1, 2, 3, 4, 5};】
        对于数组,其传递的也是 地址。
        由于此处 arrays[2]++; 是通过地址修改数据,所以原数据会被修改,
        即输出 arrays = [1, 2, 4, 4, 5]
    
    【int[] arrays2 = new int[]{1, 2, 3, 4, 5};】
        同上,但是 arrays2 = new int[]{1, 2}; 修改的是地址,所以不会影响原数据。
        即输出 arrays2 = [1, 2, 3, 4, 5]。
    
    【Test test = new Test();】
        对于自定义类型,同样传递的是 地址。
        test.a++; 通过地址修改值,会影响到原数据。
        所以输出 test = 11。

    (4)总结
      对于基本类型,参数传递为值传递,传递的为数据值,其值修改不会影响到原数据。
      对于引用类型,参数传递为引用传递,传递的为数据的地址,若直接修改此值(即修改的是地址),不会影响到原数据。但若通过地址去修改数据,则会影响到原数据。
      对于 String、Integer 类型,由于其不可变性,其值修改相当于修改了地址,所以不会影响到原数据。

    4、变量、作用域问题

    (1)问题:
      下面代码输出值为多少?(主要涉及 成员变量、局部变量、以及变量作用域问题)

    public class Main {
        public static void main(String[] args) {
            Test test1 = new Test();
            Test test2 = new Test();
            test1.test(10);
            test1.test(20);
            test2.test(30);
            System.out.println("test1: i = " + test1.i + " , j = " + test1.j + " , s = " + test1.s);
            System.out.println("test2: i = " + test2.i + " , j = " + test2.j + " , s = " + test2.s);
        }
    }
    class Test {
        static int s;
        int I;
        int j;
        {
            int i = 1;
            I++;
            j++;
            s++;
        }
        public void test(int j) {
            I++;
            j++;
            s++;
        }
    
    }

    (2)回答:
      输出如下:

    test1: i = 2 , j = 1 , s = 5
    test2: i = 1 , j = 1 , s = 5

    首先得了解一下 变量、作用域 相关知识。


    变量分类:
      成员变量,又可分为 类变量、实例变量。
      局部变量。

    成员变量、局部变量的区别:
    diff1:声明位置。
      局部变量出现在 方法体、代码块、以及形参 中。
      成员变量出现在 类中,方法体或代码块 外。
    其中:
      使用 static 修饰的成员变量称为类变量,可以使用 类名.变量名 或者 对象.变量名 获取值。
      没有 static 修饰的成员变量为实例变量,只能通过 对象.变量名 获取值。

    diff2:修饰符使用。
      局部变量最多只能被 final 关键字修饰。
      成员变量可被 static、final、public、protected、private、volatile、transient 等修饰。

    diff3:值存储的位置。
      局部变量存储在 栈中(存放基本类型数据以及 对象引用数据(对象所在堆内存的首地址))。
      实例变量存储在 堆中(存放对象实例数据)。
      类变量存储在 方法区中(存放类信息,比如常量、静态变量等)。

    diff4:生命周期、作用域
      局部变量作用域为当前代码块,每次执行时都是一个新的生命周期。
      实例变量作用域为当前对象,随对象的创建而初始化、销毁而消失。
      类变量作用域为当前类,随类的初始化而初始化、销毁而消失,且类变量在所有对象中共用。
    注:
      实例变量在当前类中使用 "this.变量名" 调用,可以缺省 this,在 其他类中使用 "对象名.变量名" 调用。
      类变量在当前类中使用 "类名.变量名" 调用,可以缺省 类名,在 其他类中 可以使用 "类名.变量名" 或者 "对象名.变量名" 调用。

    (3)分析

    【Test test1 = new Test();】
        执行时,会先加载 代码块,
        由于 s 为类变量(所有对象共用),i 为局部变量(离开代码块失效),j 为实例变量。
        所以执行后 i = 0, j = 1, s = 1.
    
    【Test test2 = new Test();】
        同理,会再次加载代码块,但是属于两个对象,所以实例变量不会相互影响。
        所以执行后 i = 0, j = 1, s = 2.
    
    【test1.test(10);】
        执行 test 方法,此时 i 为实例变量, j 为局部变量(形参), s 为类变量。
        所以执行后 i = 1, j = 1, s = 3.
    
    【test1.test(20);】
        同理,执行 test 方法。
        执行后 i = 2, j = 1, s = 4.
    
    【test2.test(30);】
        执行后 i = 1, j = 1, s = 5.
    
    所以最后输出:
        test1: i = 2 , j = 1 , s = 5
        test2: i = 1 , j = 1 , s = 5

    (4)总结
      类变量被所有实例对象所共享。
      在代码块中,若局部变量与 实例变量 重名,则需要以 this.变量名来指定 实例变量,否则会默认为局部变量。
      若局部变量 与 类变量重名,则需要以 类名.变量名 指定类变量,否则会默认为 局部变量。

    5、递归、迭代问题

    (1)问题:
      使用 递归、迭代 两种方式实现 斐波那契数列。
    斐波那契数列:

    【数列形如:】
        1, 1, 2, 3, 5, 8, 13, 21, 34,...
    
    【数学公式:】
        f(1) = 1
        f(2) = 1
        f(n) = f(n - 1) + f(n - 2)   

    当然,换一种方式描述这个问题(本质依旧是斐波那契数列):
      有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总共有多少对?

    (2)递归方式实现
      方法调用自身并解决问题的过程叫递归。
    优点:
      大问题转换为小问题,精简代码、减少代码量、可读性较好。
    缺点:
      递归浪费了大量空间,递归层数太深容易导致堆栈溢出。

    分析:
      简单将兔子按月数分为三组:1 月兔、2 月兔、3 月兔。

    【第 1 个月】
        1 月兔不能繁殖。
        兔子对数:
            1 月兔 1
            2 月兔 0
            3 月兔 0
        所以 f(1) = 1 = 1 + 0 + 0
    
    【第 2 个月】
        1 月兔变 2 月兔, 2 月兔不能繁殖。
        兔子对数:
            1 月兔 0
            2 月兔 1
            3 月兔 0
        所以 f(2) = 1 = 0 + 1 + 0
    
    【第 3 个月】
        2 月兔变 3 月兔,3 月兔可以繁殖,生出 1 对 1 月兔。
        兔子对数:
            1 月兔 1
            2 月兔 0
            3 月兔 1
        所以 f(3) = 2 = 1 + 1 + 0
    从兔子对数的变化不难看出: f(3) = f(2) + f(1)
          1 + 0 + 0
        + 0 + 1 + 0
        = 1 + 1 + 0
    
    【第 4 个月】
        1 月兔变 2 月兔,2 月兔变 3 月兔, 3 月兔可以繁殖,再生出 1 对 1 月兔。
        兔子对数:
            1 月兔 1
            2 月兔 1
            3 月兔 1
        所以 f(4) = 3
    从兔子对数的变化不难看出: f(4) = f(3) + f(2)
    
    【第五个月】
        同理,
        兔子对数:
            1 月兔 2
            2 月兔 1
            3 月兔 2
        所以 f(5) = 5
    从兔子对数的变化不难看出: f(5) = f(4) + f(3),
    从而推出公式:
        f(1) = 1,                         n = 1
        f(2) = 2,                         n = 2
        f(n) = f(n-1) + f(n-2),          n >= 3

    代码实现:

    public class Main {
        public static void main(String[] args) {
            for (int i = 1; i < 10; i++) {
                System.out.println(test(i));
            }
        }
    
        public static int test(int n) {
            if (n == 1 || n == 2) {
                return 1;
            } else {
                return test(n - 1) + test(n - 2);
            }
        }
    }

    (3)迭代方法实现
      利用变量原值计算出新值并解决问题的过程叫迭代。
    优点:
      空间开销小,运行效率比递归高。
    缺点:
      代码不够简洁、可读性较差。

    分析:
      同样将兔子按月数分为三组:1 月兔、2 月兔、3 月兔。
      使用 one 保存最后 1 月兔的对数,初始值为 1。
      使用 two 保存最后 2 月兔、3 月兔的对数,初始值为 1。
      使用 sum 保存最终数量,初始值为 0。
    可以发现一个有意思的现象。
      1 月兔在下个月会转为 2 月兔。
      2 月兔在下个月会转为 3 月兔,3 月兔会生 1 月兔。
      3 月兔在每个月均会生 1 月兔。
    也即下一次 1 月兔的数量,在于 2 月兔、3 月兔总数量。
    下一次 2 月兔、3月兔的数量,在于兔子总数量。
    所以:
      sum = one + two
      one = two
      two = sum

    代码实现:

    public class Main {
        public static void main(String[] args) {
            for (int i = 1; i < 10; i++) {
                System.out.println(test(i));
            }
        }
    
        public static int test(int n) {
            int one = 1, two = 1, sum = 0;
            if (n == 1 || n == 2) {
                return 1;
            }
            for (int i = 3; i <= n; i++) {
                sum = one + two;
                one = two;
                two = sum;
            }
            return sum;
        }
    }

    6、单例设计模式问题

      之前曾总结过一次单例设计模式,详见:https://www.cnblogs.com/l-y-h/p/11290728.html
    (1)问题:
      什么是单例设计模式?
      单例设计模式注意点、实现?
      单例设计模式的几种实现方式?

    (2)回答:
    单例设计模式(Singleton):
      指的是某个类在整个系统中只有一个实例对象能够被获取和使用的一种代码模式(比如 JVM 中的 Runtime 类)。

    单例设计模式注意点:
      某个类只能有一个实例对象。
      这个类必须自行创建其实例对象。
      这个类必须向外暴露出这个实例对象。

    单例设计模式实现:
      构造器私有化,只能在该类内部调用 new 关键字,防止外部类通过 new 关键字创建实例对象。
      使用静态变量保存这个实例对象。
      使用静态方法向外暴露出这个实例对象(静态变量私有化时必须存在,要不然无法获取到实例对象)。

    单例设计模式的几种方式:
      饿汉式:直接创建对象,不存在线程安全问题。
      懒汉式:延迟创建对象,使用时再创建,可能会有线程安全问题。

    饿汉式分类:
      静态常量版,声明变量时,直接实例化一个对象。
      静态代码块,在静态代码块中实例化对象。
      枚举型,通过枚举的方式指定,可以防止反序列化、反射问题(推荐使用)。

    懒汉式分类:
      线程不安全(直接通过静态方法返回实例对象,多线程下,可能会同时执行返回多个实例)。
      线程安全(使用同步关键字实现同步代码块、同步方法)。
      线程安全(双重检查,提高同步执行的效率)。
      线程安全(静态内部类,推荐使用)。

    (3)代码实现
    具体代码可以参考:https://www.cnblogs.com/l-y-h/p/11290728.html
    此处仅举两个例子:
      对于饿汉式来说,枚举实现最简单。
      对于懒汉式来说,静态内部类实现最简单。

    枚举形式:

    public class Main {
        public static void main(String[] args) {
            SingleTon singleTon1 = SingleTon.INSTANCE;
            SingleTon singleTon2 = SingleTon.INSTANCE;
            System.out.println(singleTon1 == singleTon2);
        }
    }
    enum SingleTon {
        INSTANCE;
    }

    静态内部类:

    public class Main {
        public static void main(String[] args) {
            SingleTon singleTon1 = SingleTon.getSingleTon();
            SingleTon singleTon2 = SingleTon.getSingleTon();
            System.out.println(singleTon1 == singleTon2);
        }
    }
    class SingleTon {
    
        /**
         * 构造器私有化(防止通过new创建实例对象)
         */
        private SingleTon() {
        }
    
        /**
         * 静态内部类,在被调用的时候才会被加载,实现懒加载。 且内部使用静态常量实例化一个对象,保证了线程安全问题。
         */
        private static class SingleTonInstance {
            private static final SingleTon INSTANCE = new SingleTon();
        }
    
        /**
         * 向外暴露一个静态的公共方法用于获取实例对象.
         */
        public static SingleTon getSingleTon() {
            return SingleTonInstance.INSTANCE; // 调用静态内部类的静态属性
        }
    }

  • 相关阅读:
    initData()
    moveUp()
    moveLeft()
    moveDown()
    函数具体分析
    Linux命令学习笔记
    RocketMQ使用记录
    solr安装记录
    centos7下面ruby的升级
    centos7下面装fastdfs
  • 原文地址:https://www.cnblogs.com/huoyz/p/14378250.html
Copyright © 2011-2022 走看看