总结Java的语法知识点,相比专栏,这些更加零碎但也非常重要

1. java中的值传递和引用传递

public static void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

实验观察:
在java中,函数修改数组,和c++一样,是引用传递,直接修改本身,不产生copy;
但是函数修改数字,是值传递,不会修改本身的值;

结论:
参考资料1,2
更正参考资料1中,最后一张图,person 指向了 X03333 不是 X02222
在面试中,一句话总结就是: Java所谓引用传递也就是传递了引用的拷贝,所以本质也是值传递。
Persontest(Person person)函数中,实际传入不是main()中的p,而是在Persontest的栈帧中,创建了一个变量person,引用copy一份赋值给它了,在里面修改person的引用,让它指向别处,不会影响main中的p指向的class的实际的值。本质还是值传递

public class PassByValue {
    public static class Person{
        private String name;
        private int age;

        public String getName(){
            return name;
        }

        public int getAge(){
            return age;
        }

        public void setName(String name){
            this.name = name;
        }

        public void setAge(int age){
            this.age = age;
        }
    }

    public static void PersonTest(Person person){
        System.out.println("传入的person的name:"+person.getName());
        person.setName("我是张三");
        System.out.println("方法内重新赋值后的name:"+person.getName());
    }

    public static void main(String[] args) {
        Person p=new Person();
        p.setName("我是李四");
        p.setAge(45);
        PersonTest(p);
        System.out.println("方法执行后的name:"+p.getName());
    }
}

执行结果:

传入的person的name:我是李四
方法内重新赋值后的name:我是张三
方法执行后的name:我是张三

修改函数:

public static void PersonTest(Person person){
    System.out.println("传入的person的name:"+person.getName());
    //person = new Person();
    person.setName("我是张三");
    System.out.println("方法内重新赋值后的name:"+person.getName());
}

执行结果:

传入的person的name:我是李四
方法内重新赋值后的name:我是张三
方法执行后的name:我是李四

2. 自动装箱(autoboxing)与拆箱(unboxing)

非常感谢Chant的博客

自动装箱(autoboxing)是指 Java 编译器自动将基本数据类型值转换成对应的包装类的对象,例如将 int 转换为 Integer 对象,将 boolean 转换问 Boolean 对象。而拆箱(unboxing)则是反过来转换。

2.1 什么时候会自动装箱和拆箱

自动装箱和拆箱会在以下这些情况发生:

  • 把基本数据类型赋值给一个声明为其包装类类型的变量时,像 Integer obj = 1 将发生装箱;反之拆箱。
  • 把基本数据类型作为函数调用的参数,而该参数的类型是其包装类时,像前面的 list.add(1); 将发生装箱;反之拆箱。
  • 对包装类进行加减乘除等基本运算时,将发生拆箱。像 Integer a = 0; a += 1; 在执行 += 运算的 + 运算时将发生拆箱。

2.2 可能带来的问题, 值传递

  1. 对象比较的结果
    考虑以下代码
    public class TestJava {
    public static void main(String[] args) {
            Integer number1 = 127;
            Integer number2 = 127;
            System.out.println("number3 == number4: " + (number3 == number4));
    }
    }
    输出:
    number1 == number2: true
    创建两个不同的对象,注意这里是用 == 比较两个 Integer 对象的内存指针,不是比较他们代表的值。number1 和 number2 是两个不同的对象,比较结果应该是 false。其实是因为 Java 虚拟机对 -128到127 的 Integer 对象做了缓存,所以 number1 和 number2 实际上是同一个对象,比较结果自然也就返回 true。
    Java 对大整形Integer,-128到127,是 值传递⚠️
  2. 对象的过多创建
    Integer sum = 0;
    for (int i = 0; i < 1000; i++) {
     sum = sum + i;
    }
    上面的代码中,由于 sum 是一个 Integer 对象,不能直接进行 + 操作,所以会先执行 sum.intValue() 拆箱,得到 int 类型进行 + 操作。然后再执行 Integer.valueOf(sum) 进行装箱操作,得到一个 Integer 对象赋值给 sum。虽然 Java 虚拟机对 -128 到 127 的 Integer 对象做了缓存,但是从第 128 到 1000 次循环中总共还要创建 873 个对象。这将导致程序的性能降低甚至触发垃圾回收。而简单地把 sum 的类型改为 int 就可解决问题。

3. 为什么弃用Stack类

参考文章
Stack类的源码中有,public class Stack<E> extends Vector<E>,这就意味着它继承与Vector类,Vector都不要了,它自然也不用了。java官方建立不要使用老旧的 stack类,而使用Deque类去取代它

Deque<TreeNode> stack = new ArrayDeque<>();
// 压入
stack.push(cur);
// 弹出
cur = stack.pop();
// 栈顶元素
cur = stack.peek();

4. 重载和重写的区别

◼ 名称 重载英文是overload,重写英文是override。
◼ 作用

  1. 重载发生在同一个类的多个同名方法之间,让代码便于调用; 具体定义可以参考java基础第一章
  2. 重写发生在子类与其父类的同名方法之间,让代码易于复用
    父类与子类之间也存在重载,即在子类中定义出与父类名称相同、参数列表不同的方法。

5. super关键字注意点

5.1 super关键字是什么

特别注意,super不是其父类对象的引用!在“java核心技术卷1”原书第10版第五章的5.1.2节注释中提到,super不是对象的引用,只是一个指示编译器调用超类方法的特殊关键字。具体讨论参考这里

public void testTwo() {
        Base base = super; //无法赋值成功,IDE提示错误,因为super根本不是父类对象引用
        Object object = super; //同理,即便是赋值给Object这个顶层超类也不行
        int num = super.num; //super可访问父类成员变量(不仅可以访问类成员变量,还可以访问实例成员变量)
        int methodResultValue = super.baseTest(); //super可访问父类成员方法(不仅可以访问类成员方法,还可以访问实例成员方法)
        }

5.2 子类构造器执行顺序

在调试含有父类的构造器时发现了,子类的构造器中如果没有特别申明,会按照:

super() -> 初始化成员变量 -> 其他内容

的方式执行。也就是说先调用父类的构造器,再初始化成员变量,最后在执行后面的代码。原因就是后续的代码可能会依赖父类的构造器,因此super()必须在第一行

5.3 父类无参构造器

创建子类的对象实例时,默认会先调用父类的无参数构造器(默认构造函数)。如果父类没有指明无参数构造器,JVM会创建一个默认的,前提是不存在有参数构造器。

// 父类
public class Person {
    String name;

    public Person(){
        System.out.println("Init person");
    }

    public Person(String name){
        this.name = name;
        System.out.println("Init person with name");
    }
}

// 子类
public class Driver extends Person{

    public Driver(){
        // 这里无论写不写super(),都自动会调用super()
        super();
        System.out.println("Init driver");
    }

    public Driver(String name){
        // 如果没有调用父类带参数构造器,JVM会自动调用父类无参数构造器
        // 这里无论写不写super(),都自动会调用super(),除非调用父类有参数构造器
          super();
          // 此时不会调用super(),而是直接调用super(name)
          // super(name);
        System.out.println("Init driver with name");
    }

      public static void main(String[] args) {

          Driver d1 = new Driver();
        Driver d2 = new Driver("Tom");

    }
}

因此,当子类中有参数构造器时用super()输出就是:

Init person
Init driver
Init person
Init driver with name

当子类中有参数构造器指定了父类的有参数构造器super(name)输出就是:

Init person
Init driver
Init person with name
Init driver with name

常见的错误就是,父类只定义了有参数构造器,没有构造无参数构造器,此时子类的构造函数就会报错,指出父类无参数构造器没有申明。这是因为,创建类时指定了有参数构造函数后,系统默认不会创建无参数构造函数,需要自己手动创建。

// 父类
public class Person {
    String name;
    public Person(String name){
        this.name = name;
        System.out.println("Init person with name");
    }
}

// 子类
public class Driver extends Person{

    public Driver(){
        // 这里无论写不写super(),都自动会调用super()
        super();
        System.out.println("Init driver");
    }

    public Driver(String name){
          super(name);
        System.out.println("Init driver with name");
    }

      public static void main(String[] args) {

        // 编译不通过!
        Driver d1 = new Driver();
        Driver d2 = new Driver("Tom");

    }
}

输出:编译错误,没有 super constructor Person()

解决方法:

A:在父类中加一个无参构造方法
B:通过使用super关键字去显示的调用父类的带参构造方法
C:子类通过this去调用本类的其他构造方法
子类中一定要有一个去访问了父类的构造方法,否则父类数据就没有初始化。

6.equals和==的区别

参考转载自千古壹号,总结得特别好!

一、java当中的数据类型和“==”的含义:

  1. 基本数据类型(也称原始数据类型) :byte,short,char,int,long,float,double,boolean。他们之间的比较,应用双等号(==),比较的是他们的值

  2. 引用数据类型:当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址(确切的说,是堆内存地址)。
    注:对于引用类型,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。因为每new一次,都会重新开辟堆内存空间。

特别注意如下情况,

Integer number1 = new Integer(127);
Integer number2 = new Integer(127);
System.out.println("number1 == number2: " + (number1 == number2));
Integer number3 = 127;
Integer number4 = 127;
System.out.println("number3 == number4: " + (number3 == number4));

String str1 = "hellow";
String str2 = "hellow";
System.out.println("str1 == str2: " + (str1 == str2));
String str3 = new String("hellow");
String str4 = new String("hellow");
System.out.println("str3 == str4: " + (str3 == str4));

/*
number1 == number2: false 
number3 == number4: true 常量池缓存
str1 == str2: true 常量池缓存
str3 == str4: false
*/

二、equals()方法介绍:

JAVA当中所有的类都是继承于Object这个超类的,在Object类中定义了一个equals的方法,equals的源码是这样写的:

public boolean equals(Object obj) {
    //this - s1
    //obj - s2
    return (this == obj);
}

可以看到,这个方法的初始默认行为是比较对象的内存地址值,一般来说,意义不大。所以,在一些类库当中这个方法被重写了,如String、Integer、Date。在这些类当中equals有其自身的实现(一般都是用来比较对象的成员变量值是否相同),而不再是比较类在堆内存中的存放地址了。
所以说,对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是内存中的存放位置的地址值,跟双等号(==)的结果相同;如果被复写,按照复写的要求来。

我们对上面的两段内容做个总结吧:

  1. ==的作用:
      基本类型:比较的就是值是否相同
      引用类型:比较的就是地址值是否相同
  2. equals的作用:
      引用类型:默认情况下,比较的是地址值。但是某些类重写了equals()方法,比如Interger, String, 它们比较的是内容是否一样。

此外当我们自定义类时,可以根据情况自己重写equals(),来比较对象的成员变量值是否相同,比如自定义student类,定义当年龄,年纪,姓名都一样时是一个学生。

三、String类的equals()方法:

  • ==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;
  • equals():比较的是两个字符串的内容,属于内容比较。

以后进行字符串相等判断的时候都使用equals()