Java基础问题

JDK、JRE、JVM三者的区别和联系

  • JDK:Java Development Kit,Java开发工具包,提供了Java的开发环境和运行环境;包含了编译Java源文件的编译器Javac,还有调试和分析的工具。

  • JRE:Java Runtime Environment,Java运行环境,包含Java虚拟机及一些基础类库

  • JVM:Java Virtual Machine,Java虚拟机,提供执行字节码文件的能力

    JVM是实现Java跨平台的核心,由其可实现java程序 “一处编译,到处运行”,但JVM本身不是跨平台的,不同的平台需要安装不同的JVM

Java基本数据类型和变量类型有哪些?

  • 基本数据类型:boolean, char, byte, short, int, long, float, double

    注意:String是引用类型

  • 变量类型:

    • 局部变量:定义在方法体内部的变量

    • 成员变量:

      • 类变量(静态变量):独立于方法之外的变量,属于类本身。需要static修饰,类变量就是以static修饰的独立于方法之外的成员变量

      • 实例变量(非静态变量):独立于方法之外的变量,依赖实例存在。不需要static修饰,实例变量就是没有static修饰的独立于方法之外的成员变量

      • 常量

值传递和引用传递

  1. 基本数据类型使用的时值传递,方法结束栈帧出栈,数据消失

  2. 引用类型(String、数组、对象)使用的时引用传递,传递的实际是指向同一内存区域的地址

== 和 equals 的区别

  • == 对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址

  • equals:Object中默认也是采用 == 比较,通常会重写;比如在String中,equals比较的就是内容,即字符串的值

 public class StringDemo{
  public static void main(){
  String str1 = "Hello"; // JVM会将其分配到常量池
  String str2 = new String("Hello");  // 分配到堆内存
  String str3 = str2;  // 引用传递
  System.out.println(str1 == str2); //false
  System.out.println(str1 == str3); //false
  System.out.println(str2 == str3); // true
  System.out.println(str1.equals(str2)); //true
  System.out.println(str1.equals(str3));  //true
  System.out.println(str2.equals(str3)); //true
  }
 }

重载和重写

  • 重载:生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同(混淆点:跟返回类型没关系),发生在编译时

     public int add(int a, int b);
     public String add(int a, int b);
     // 不是重载,编译报错
  • 重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果分类方法访问修饰符为private则子类就不能重写该方法

谈谈对面向对象的理解

  • “面向对象” vs “面向过程”

    二者是思考角度的差异,面向过程以“执行者”的角度来思考问题,更看重事情的每一个步骤及顺序;而面向对象更多以“组织者”的角度来思考问题,更注重事情有哪些参与者(对象)、及各自需要做什么.

    举个简单的生活例子,比如用洗衣机洗衣服:

    面向过程会将任务拆解为一系列步骤(函数):打开洗衣机 -> 放衣服 -> 倒洗衣液 -> 清洗 -> 甩干

    面向对象会拆解出人和洗衣机两个对象:人有三个方法:打开洗衣机,放衣服,倒洗衣液;洗衣机有两个方法:清洗,甩干

    由此可以看出面向对象的更多是考虑如何去选择合适的工具,然后组织到一起干一件事;在程序世界中,这个思维无处不在,比如,我们要开发项目,以三层架构的模式来开发,那么这个时候,我们不需要重复造轮子,只需要选择市面上的主流框架即可,比如:Spring,Spring MVC,Mybatis等等

    正因如此,面向对象更易于复用、扩展和维护

    面向对象编程的本质就是:以类的方式组织代码,以对象的形式组织(封装)数据

  • 面向对象的三大特性:封装性、继承性、多态性

    • 封装性:封装的意义在于明确标识出允许外部使用的所有成员函数和数据项;内部细节对外部调用透明,外部调用无需修改或者关心内部表现

      两个经典的封装性的运用:

      1. javabean的属性私有,一组get/set对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决定,而不能由外部胡乱修改

         // 假设有一个Javabean有如下属性与其对应的set方法
         // 可以看出该name有自己的命名规则,外部调用必须遵从其命名规范,无法直接赋值
         private String name;
         public void setName(String name){
          this.name = "Jarreet_"  + name;
         }
      2. orm框架,操作数据库,我们不需要关心链接是如何建立的、sql是如何执行的,只需要引入mybatis,调方法即可

    • 继承性:继承基类的方法,并做出自己的改变和/或扩展;子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的;便于代码复用

    • 多态性:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同;易于程序维护和扩展

      多态的三个条件:继承,方法重写,父类引用指向子类

       // 注意:无法调用子类特有的功能
       父类类型 变量名 = new 子类对象;
       变量名.方法名();
       // 原生的javaweb开发中就有经典的多态性的运用,如
       UserDao userDao = new UserDaoImpl();

向上转型和向下转型

  • 向上转型:将子类对象转换为父类对象,此处父类对象可以是接口(安全的)

  • 向下转型:把父类对象转换为子类对象(不安全的)

 // 有两个类 Father是父类 Son是子类
 Father f1 = new Son() // f1指向一个Son对象,向上转型
 Son s1 = (Son)f1 // f1指向Son对象
 
 Father f2 = new Father();
 Son s2 = (Son)f2; // 报错;子类引用不能指向父类对象
  • 注意点:

    • 父类引用指向子类对象,而子类引用不能指向父类对 象

    • 把子类对象直接赋给父类引用叫upcasting向上转型,向上转型不用强制转化

    • 把指向子类对象的父类引用赋给子类引用叫downcasting向下转型,需要强制转换

this关键字和super关键字

  1. this代表本身调用者这个对象;this()代表本类的构造。

  2. super代表父类对象的引用;super()代表父类的构造;子类初始化时会自动调用父类的无参构造器,若父类没有无参构造器,则子类需要手动调用父类的有参构造器super(param)且该语句应放在子类构造器的首行,否则报错。

接口和抽象类的区别

  • 接口和抽象类的特性

    • 抽象类可以存在普通成员函数,而接口只能存在 public abstract 方法

    • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的

    • 抽象类只能继承一个,接口可以实现多个

  • 接口和抽象类的设计目的和使用场景

    • 接口的设计目的,是对类的行为进行一种“有”约束,也就是提供一种机制,可以强制要求不同的类具有相同的行为;它只是约束了行为的有无,但不对如何实现行为进行限制

    • 抽象类的设计目的,是代码复用;当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方法一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类;在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的;而A减B的部分,留给各个子类自己实现;正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到 A-B 时,无法执行)

    • 抽象类是对类的本质的抽象,表达的是 is a 的关系,比如:BWM is a Car;抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现

    • 接口是对行为的抽象,表达的是 like a 的关系;比如:Bird like a AirCraft,但其本质上 is a Bird;接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关系

    • 使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口

    抽象类的功能要远超过接口;但是,定义抽象类的代价太高;因为对高级语言来说,每个类只能继承一个类;在这个类中,你必须继承或编写出其所有子类的所有共性;虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述;而且你可以在一个类中同时实现多个接口;在设计阶段会降低难度

包装类

  1. 什么是包装类?

    包装类是java提供的一组类,专门用来创建8哥基本数据类型对应的对象,一共有8哥包装类,存放在java.lang包中,基本数据类型和其对应的包装类如下:

    基本数据类型包装类
    byte Byte
    short Short
    int Integer
    long Long
    float Float
    double Double
    char Character
    boolean Boolean
  2. 包装类的体系结构

    • Object有三个子类:Character、Number、Boolean

    • Number又衍生出六个子类:Byte、Short、Integer、Long、Float、Double

  3. 装箱和拆箱

    装箱和拆箱是包装类的特有名词,装箱时指将基本数据类型转为对应的包装类对象,拆箱就是将包装类对象转为对应的基本数据类型。

    • 装箱:

      每个包装类都提供了一个有参构造函数:public Type(type value),用来实例化包装类对象(该方法已过期,不推荐使用)

       // int类型数据的装箱,其余类型类似
       int i = 8;
       Integer integer = new Integer(i);

      每个包装类还有一个重载的构造函数,Character的重载构造函数:public Type(char value),其他包装类的重载构造函数:public Type(String value)(该方法已过期,不推荐使用)

       // Character的重载构造函数
       Character character = new Character("a");
       
       // Integer的重载构造函数,其余包装类相似
       Integer integer = new Integer("8");
       
       // 在Boolean的重载构造函数中,党参数为"true"时,Boolean值为true,否则为flase
       Boolean flag = new Boolean("a");

      每一个包装类都有一个valueOf(type value)静态方法(推荐使用本方法)

       // Integer的valueOf()方法,其余包装类类似
       int i = 8;
       Integer integer = Integer.valueOf(i);

      同样,valueOf也有重载方法,valueOf(char value)专门为Character服务,其他7个包装类则使用valueOf(String value)

      Character character = Character.valueOf("a");

      Integer integer = Integer.valueOf("8");

      Boolean flag = Boolean.valueOf("a");
    • 拆箱:

      每个包装类都有一个 *Value()方法,通过该方法可以将包装类转为基本数据类型

      // Integer的*value()方法,其余类似
      int i = integer.intValue();

      除了Character外,每个包装类都有一个parse*(String value)方法可以将字符串类型转为基本数据类型

      // Integer的parse*()方法,其余类似
      int i = Integer.pareInt("8");
  4. 自动装箱和自动拆箱

    • 自动装箱:自动把基本数据类型转为包装类

      // 自动装箱
      Integer integer = 0;
    • 自动拆箱:自动把包装类转为基本数据类型

      // 自动拆箱
      int i = integer;
  5. 经典面试题:

        @Test
    public void testInteger() {
    Integer i1 = new Integer(12);
    Integer i2 = new Integer(12);
    System.out.println(i1 == i2); // false

    Integer i3 = 126;
    Integer i4 = 126;
    int i5 = 126;
    System.out.println(i3 == i4); // true
    System.out.println(i3 == i5); // true

    Integer i6 = 128;
    Integer i7 = 128;
    int i8 = 128;
    System.out.println(i6 == i7); // false
    System.out.println(i6 == i8); //true
    }

内部类

一般情况下,类和类之间是相互独立的,内部类的意思是打破这种独立,让一个类成为另外一个类的内部成员,和成员变量、成员方法同等级别。

采用内部类这种技术,可以隐藏细节和内部结构,封装性更好,让程序的结构更加合理。

基本的内部类既可以在外部类中直接定义,也可以在外部类中的方法体中定义。

  1. 非静态内部类

    package com.jarreet.test;

    public class OuterClass{
    // 成员变量
    private String outerName;
    // 成员方法
    public void display(){
    System.out.println("OuterClass display");
    System.out.println(outerName);
    }
    // 内部类
    public class InnerClass{
    private String innerName;
    public void display(){
    System.out.println("InnerClass display");
    System.out.println(innerName);
    }

    public InnerClass(){
    innerName = "inner Class";
    }
    }

    public static void main(String[] args){
    OuterClass outerClass = new OuterClass();
    outerClass.display();
    }
    }

    非静态内部类的使用就是将内部类当作外部类的一个成员变量/成员方法来使用,所以必须依赖于外部类的对象才能调用,用法和成员变量/成员方法是一致的。

  2. 静态内部类

    静态内部类的构造不需要依赖于外部类对象,类中的所有静态组件都不需要依赖于任何对象,可以直接通过类本身进行构造。

    package com.jarreet.test;

    public class OuterClass{
    // 成员变量
    private String outerName;
    // 成员方法
    public void display(){
    System.out.println("OuterClass display");
    System.out.println(outerName);
    }

    // 静态内部类
    public static class InnerClass{
    private String innerName;
    public InnerClass(){
    innerName = "inner class";
    }
    public void display(){
    System.out.println("InnerClass display");
    System.out.println("innerName");
    }
    }

    public static void main(String[] args){
    OuterClass outerClass = new OuterClass();
    outerClass.display();
    InnerClass innerClass = new InnerClass();
    innerClass.display();
    }
    }
  3. 匿名内部类

    • 定义接口

      package com.jarreet.test;

      public interface MyInterface {
      public void test();
      }
    • 使用匿名内部类

      package com.jarreet.test;

      public class Test {
      public static void main(Stirng[] args){

      // 匿名内部类
      MyInterface myInterface1 = new MyInterface() {
      @Override
      public void test(){
      System.out.println("test");
      }
      };
      myInterface1.test();
      }
      }

    匿名内部类在多线程中用到的比较多,且多和lambda表达式配合使用

final关键字

  • final关键字的作用

    final可以用来修饰类、方法以及变量;修饰类表示该类不可被继承;形式方法表示方法不可被子类覆盖,但是可以重载;修饰变量表示变量一旦被赋值就不可以更改它的值。

    1. 修饰成员变量

      如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值;如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值

    2. 修饰局部变量

      系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化;因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋值初值(仅一次)

      public class FinalVar{
      final static int a = 0; // 在声明的时候就要赋值 或者静态代码块赋值
      /**
      static{
      a = 0;
      }
      */

      final int b = 0; // 在声明的时候就要赋值 或者代码块中赋值 或者构造器赋值
      /**
      {
      b = 0;
      }
      */

      public void main(String[] args){
      final int localA; // 局部变量只声明没有初始化,不会报错,与final无关
      localA = 0; // 在使用前一定要赋值
      // localA = 1; // 但是不允许第二次赋值
      }
      }
    3. 修饰基本数据类型和引用数据类型

      如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象;但是引用的值是可变的

      public class FinalReferenceTest(){
      public static void main(){
      final Person p = new Person(25);
      p.setAge(24); // 合法
      // p = null; // 非法
      }
      }
  • 为什么局部内部类和匿名内部类只能访问局部final变量?

    // 编译之后,会生成两个class文件,Test.class Test1.class

    public class Test{
    public static void main(String[] args){
    // 局部final变量a,b
    public void test(final int b){
    final int a = 10;
    // 匿名内部类
    new Thread(){
    public void run(){
    System.out.println(a);
    Ststem.out.println(b);
    };
    }.start();
    }
    }

    class Outclass{
    private int age = 12; // 成员变量不用经过final修饰也能访问

    public void outPrint(final int x){ // 局部变量需要final修饰才能访问
    class InClass{
    public void InPrint(){
    System.out.println(x);
    Ststem.out.println(age);
    }
    }
    new InClass().InPrint();
    }
    }
    }

    首先我们要知道的一点是:内部类和外部类是处于同一级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁.

    这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象还可能存在(只有没人再引用它时,才会死亡);这里就出现了一个矛盾:内部类对象访问了一个不存在的变量;为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样党局部变量死亡后,内部类仍然可以访问它,实际访问的是局部变量的 "copy"。这样就好像延长了局部变量的生命周期。

    将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?

    就将局部变量设置为fianl,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性;这实际上也是一种妥协;使得局部变量与内部类内建立的拷贝保持一致。

posted @ 2021-05-06 00:31  离渊灬  阅读(68)  评论(0)    收藏  举报