强类型语言和弱类型语言:

  • 强类型语言是强制定义数据类型的语言。它的一个变量一旦指定了某种数据类型,如果不经过转换,那该变量就永远是此数据类型了,强类型语言是类型安全的语言。 比如 int a,那么程序只会认为 a 这个变量是整型,而不可能把它作为字符来处理。
  • 弱类型语言是数据类型是可以忽略的的语言。它的一个变量可以被定义长多种数据类型,而且不用进行转换,弱类型语言是类型不安全的语言。 比如 var a,这个 a 可以被赋值成整型,也可以被赋值成字符型,还可以是其他类型。

强类型语言在速度上可能会稍逊色弱类型语言,但是强类型语言带来的严谨性能够有效的避免许多错误的发生。

1 Java 中的数据类型

Java 虚拟机中,数据类型可以分为两类:基本类型和引用类型。

基本类型的变量代表的就是数值本身。而引用类型的变量代表的是这个对象的地址,而非对象本身。

基本类型包括:byte、short、int、long、float、double、char、boolean

基本数据类型

引用数据类型非常多,大致包括:类、接口类型、数组类型、枚举类型、注解类型、字符串类型。例如,String 类型就是引用类型。简单来说:所有的非基本数据类型都是引用数据类型。

2 Java 中的变量类型

  • 类变量:独立于方法之外的变量,用 static 修饰
  • 实例变量:独立于方法之外的变量,没有 static 修饰
  • 局部变量:类的方法中的变量
class Variable {
  static int allClicks = 0; // 类变量
  String str = "hello world"; // 实例变量

  public void method() {
    int i = 0; // 局部变量
  }
}
2.1 局部变量
  • 局部变量声明在方法、构造方法或者语句块中;局部变量只在声明它的方法、构造方法或者语句块中可见;
  • 局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用;
  • 局部变量在方法、构造方法或者语句块被执行的时候创建,当它们执行完成后,变量会被销毁;
  • 访问修饰符不能用于局部变量;
  • 局部变量是在栈上分配的

局部变量

2.2 实例变量
  • 实例变量声明在一个类中,但在方法、构造方法和语句块之外;
  • 实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有。通过使用访问修饰符或者被某个方法引用可以使实例变量对外部可用;
  • 实例变量具有默认值。数值型变量的默认值是 0,布尔型变量的默认值时 false,引用类型的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定;
  • 实例变量在对象创建的时候创建,在对象销毁的时候销毁;
  • 访问修饰符可以修饰实例变量;
2.3 类变量(静态变量)
  • 类变量也称为静态变量,在类中用 static 关键字声明,但必须在方法、构造方法和语句块之外;
  • 无论一个类创建了多少个对象,类只拥有类变量的一份拷贝;
  • 静态变量除了被声明为常量(public/private static final)外很少使用,常量初始化后不可改变;
  • 静态变量存储在方法区,但是很少单独使用 static 声明变量;
  • 静态变量在程序开始时创建,在程序结束时销毁;
  • 与实例变量具有相似的可见性,但为了对类的使用者可见,大多数静态变量声明为 public 类型;
  • 默认值和实例变量相似。数值型变量默认为 0,布尔型默认值为 false,应用类型默认值是 null。变量的值可以在声明的时候指定,也可以在构造方法中指定。此外,静态变量还可以在静态语句块初始化。
  • 静态变量可以通过 ClassName.VariableName 的方式访问
  • 类变量被声明为 public static final 类型时,类变量名称一般建议使用大些字母。如果静态变量不是 public 和 final 类型,其命名方式与实例变量以及局部变量的命名方式一致
2.4 变量的存储位置

当执行一个 Java 方法时,JVM 会压入一个新的栈帧到该线程的 Java 虚拟机栈中,在该方法执行完成后,这个栈帧就从 Java 虚拟机栈中弹出,在方法中声明的变量也会随着栈帧的销毁而销毁,这就是局部变量只在方法中有效的原因。

在局部变量可以是基本类型的变量,也可以是引用类型的变量:

  • 当声明的是基本类型的变量时,该变量名及值是放在 Java 虚拟机栈中的;
  • 当声明的是引用类型的变量时,所声明的变量是放在 Java 虚拟机栈中,而该变量所指向的对象是放在堆内存中的;
class Variable{
    public static void main(String[] args) {
        int i = 1;
        double d = 1.2;

        String str = "helloworld";
    }
}

实例变量/成员变量,放在堆中(因为成员变量不会随着某个方法执行结束而销毁)

在类中声明的变量既可以是基本类型的变量,也可以是引用类型的变量:

  • 当声明的是基本类型的变量时,其变量名及值时放在堆内存中的;
  • 当声明的是引用类型的变量时,其声明的变量会存储一个内存地址的值,该值指向所引用的对象。变量名存储在栈中,对应的对象存储在堆中;

类变量(静态变量)存储在方法区,但是很少单独使用 static 声明变量

分析以下代码中的局部变量、实例变量和静态变量在方法区、栈内存、堆内存中的分配:

class BigWaterMelon {

  public int weight; 

  public BigWaterMelon(int weight) {
    this.weight = weight;
  }
}

class Fruit {
  static int x = 10; 
  static BigWaterMelon bigWaterMelon_1 = new BigWaterMelon(x); 

  int y = 20;
  BigWaterMelon bigWaterMelon_2 = new BigWaterMelon(y);

  public static void main(String[] args) {
    final Fruit fruit = new Fruit();

    int z = 30; 
    BigWaterMelon bigWaterMelon_3 = new BigWaterMelon(z); 

    new Thread() {
      @Override
      public void run() {
        int k = 100;
        setWeight(k);
      }

      void setWeight(int waterMelonWeight) {
        fruit.bigWaterMelon_2.weight = waterMelonWeight;
      }
    }.start();
  }

}

同一种颜色代表变量和对象的引用关系:

内存分配

由于方法区和堆内存的数据都是线程间共享的,所以线程 Main Thread,New Thread 和 Another Thread 都可以访问方法区中的静态变量以及访问这个变量所引用的对象的实例变量。(方法区存储的都是只加载一次的)。

每个线程都有自己的虚拟机栈,每一个栈中的数据都是线程独有的,也就是说 New Thread 中的 setWeight(int) 方法是不能访问线程 Main Thread 中的局部变量 bigWaterMelon_3。

但是 setWeight(int) 却访问了同为 Main Thread 局部变量 fruit,这是因为 fruit 被声明为 final 了,fruit 被声明为 final 后,fruit 会作为 new Thread 构造函数的一个参数传入 New Thread,也就是堆内存 Fruit 1 对象中的实例变量 v a l 1 对象中的实例变量 val 1对象中的实例变量valfruit 会引用 fruit 引用的对象,从而 New Thread 可以访问 Main Thread 局部变量 fruit。

3 Java 中方法的参数传递

Java 中参数传递:不管是基本类型还是引用类型,形参做赋值操作的时候,实参不发生改变,如果是在方法中改变形参的一些成员变量,那么实参也会发生变化。

参数是基本数据类型时:

class Variable {
  public static void main(String[] args) {
    int msg = 100;
    System.out.println("调用fun方法前的msg: " + msg); // 100
    fun(msg);
    System.out.println("调用fun方法后的msg: " + msg); // 100
  }

  public static void fun(int temp) {
    temp = 0;
  }
}

基本类型的变量传递方法

参数为引用类型时:

class Variable {
    public static void main(String[] args) {
        Book book = new Book("Java开发指南", 66.6);
        book.getInfo(); // 图书名称:Java开发指南价格:66.6
        fun(book);
        book.getInfo(); // 图书名称:Java开发指南价格:99.9
    }

    public static void fun(Book temp) {
        temp.setPrice(99.9);
    }
}

class Book {
    String name;
    double price;

    public Book(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public void getInfo() {
        System.out.println("图书名称:" + name + "价格:" + price);
    }
}

调用时为 temp 在栈中开辟新空间,并执行 book 的具体内容,方法执行完毕后 temp 在栈中的内存被释放掉:

引用类型变量传递方式

当被调用的方法开始执行的时候,就从方法区里把方法拿到栈内存中,形参变量也会根据传递的值进行初始化,当方法执行结束,变量就销毁了,所以只是在栈内对形参进行了改变,而并没有影响到实际参数。所以不管参数基本类型还是引用类型,都是对栈内存中的变量进行了改变,并没有影响到实际参数。

对于引用类型,参数传递的是变量的引用地址,指向堆中的对象。如果在方法中,通过这个地址访问对象并改变了对象的某些属性,即使是变量随着方法执行结束而销毁,堆中对象的某些属性也已经发生改变了,所以实际参数发生了改变。

基本类型数据传递是值传递,对象传递的是引用的地址,所以还是按值传递。

Demo1

class Variable {

  public static void main(String[] args) {
    int a = 10;
    change(a);
    System.out.println(a); // 10

    String str = "abc";
    change(str);
    System.out.println(str); // abc

    Student s = new Student("虚竹", 13);
    System.out.println(s); // Student{name='虚竹', age=13}
    System.out.println(s.hashCode()); // 1209271652
    change(s);
    System.out.println(s); // Student{name='虚竹', age=13}
    System.out.println(s.hashCode()); // 1209271652
    change1(s);
    System.out.println(s); // Student{name='段誉', age=14}
    System.out.println(s.hashCode()); // 1209271652
  }

  public static void change(String s) {
    s = s + "def";
  }

  public static void change(int a) {
    a = a + 10;
  }

  public static void change(Student s) {
    s = new Student("萧峰", 14);
  }

  public static void change1(Student s) {
    s.setName("段誉");
    s.setAge(14);
  }
}

class Student {
  String name;
  int age;

  public Student(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public String getName() {
    return name;
  }

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

  public int getAge() {
    return age;
  }

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

  @Override
  public String toString() {
    return "Student{" +
      "name='" + name + '\'' +
      ", age=" + age +
      '}';
  }
}

Demo2

class Variable {

  public static void main(String[] args) {
    int a = 10;
    int b = 20;
    System.out.println(a + " " + b); // 10 20
    change1(a, b);
    System.out.println(a + " " + b); // 10 20
    System.out.println("==============");

    Student s = new Student();
    System.out.println(s); // Student{name='null', age=0}
    change2(s);
    System.out.println(s); // Student{name='haha', age=1}
    System.out.println("==============");

    String string = "abc"; // 字符串"abc"放在常量池中,常量池中的值是不能改变的
    System.out.println(string); // abc
    change3(string);
    System.out.println(string); // abc
    System.out.println("==============");

    StringBuffer sb = new StringBuffer("hello");
    System.out.println(sb); // hello
    change4(sb);
    System.out.println(sb); // helloworld
    System.out.println("==============");

    int[] arr = {1, 2, 3, 4};
    System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4]
    change5(arr, 0, 1);
    System.out.println(Arrays.toString(arr)); // [2, 1, 3, 4]
    System.out.println("==============");

    StringBuffer sb1 = new StringBuffer("abcd");
    StringBuffer sb2 = new StringBuffer("efgh");
    change6(sb1, sb2);
    System.out.println(sb1); // abcd
    System.out.println("==============");

    Student s1 = new Student("hehe", 1);
    Student s2 = new Student("haha", 20);
    change7(s1, s2);
    System.out.println(s1); // Student{name='hehe', age=1}
  }

  private static void change1(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
  }

  private static void change2(Student s) {
    s.setName("haha");
    s.setAge(1);
  }

  private static void change3(String string) {
    string += "def";
  }

  private static void change4(StringBuffer sb) {
    sb.append("word");
  }

  private static void change5(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
  }

  private static void change6(StringBuffer sb1, StringBuffer sb2) {
    sb1 = sb2;
  }

  private static void change7(Student s1, Student s2) {
    s1 = s2;
  }
}

4 Java 中深拷贝和浅拷贝

首先需要明白,浅拷贝和深拷贝都是针对一个已有对象的操作。在 Java 中,除了基本数据类型(元类型)之外,还存在类的实例对象这个引用数据类型。而一般使用 = 号做赋值操作的时候。对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,实际上还是指向的同一个对象。而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。因此,所谓的浅拷贝和深拷贝,只是在拷贝对象的时候,对类的实例对象这种引用数据类型的不同操作而已。

总结来说:

  • 浅拷贝:对引用数据类型进行引用传递般的拷贝,此为浅拷贝;
  • 深拷贝:对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝;

对于基本类型而言,只有值传递。

参考

https://www.cnblogs.com/joyfulcode/p/11822369.html
https://www.cnblogs.com/maskwolf/p/9972982.html
https://blog.csdn.net/BlackPlus28/article/details/108663036
https://blog.csdn.net/lilong117194/article/details/79421990?utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link
https://www.cnblogs.com/xingzc/p/5756119.html
https://zhuanlan.zhihu.com/p/57822875
https://www.cnblogs.com/xingzc/p/5756119.html
https://blog.csdn.net/HD243608836/article/details/87940295

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐