一:Java语言的跨平台性与字节码概述

JVM,机器码与字节码

JVM 即: Java Virtual Machine 也就是 Java 虚拟机。

Java 语言有一个特点:平台无关性JVM 就是实现这一个特点的关键。

我们知道,软件运行依赖于操作系统(Operating System)。早期的开发者使用的程序语言并不具备良好的移植性,如果想在不同的操作系统平台上面运行功能相同的应用,需要为每一种平台都编写可以被平台识别认知的代码。一般编译器会直接将程序的源代码编译成计算机可以直接执行的 机器码

1、Java程序的基本结构 - 图1

Java 语言则具有平台无关性,也就是所谓的 Write Once,Run Anywhere (一次编译,到处运行)。Java 编译器并不是将 Java 源代码编译为由 0,1 序列构成的计算机可直接执行的机器码,而是将其编译为扩展名为 .class 的字节码。如果想要执行字节码文件,平台上面必须安装 JVMJVM 解释器会将字节码解释成依赖于平台的机器码。

1、Java程序的基本结构 - 图2

从上图也可以看出,不同操作系统需要安装基于该操作系统的 JVMJVM 屏蔽了操作系统之间的差异,实现了 Java 语言的跨平台性。

二:Java语言的基本单元——类与包

类(class)

Java 语言中,类是最小的基本单元

一个最简单的类

  1. public class Cat {
  2. }

包(package)

为了更好地组织类,Java 语言提供了包的机制,用于区别类名的命名空间。

示例:

一个属于my.cute 包下的 Cat

  1. package my.cute;
  2. public class Cat {
  3. }

Java 语言中,包一般会用域名的反序来命名。

例如:

  1. package com.alibaba.fastjson;

这样可以避免类名冲突。

三:Java语言的基本结构——包的意义

包的意义与作用:

  1. 把功能相似或相关的类组织在同一个包中,方便查找与管理
  2. 同一个包中类名要求不能相同,但是不同包中的类名可以相同;当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包也可以避免类名冲突。
    示例:
    在不同包下有着相同类名的类,我们可以使用全限定类名(Full Qualified Name)加以区分。 ```java package com.github.hcsp;

public class Home { com.github.hcsp.pet1.Cat cat1; com.github.hcsp.pet2.Cat cat2; }

  1. 3. 包限定了访问权限
  2. <a name="b691a954"></a>
  3. ### 四:在Java中引入第三方包
  4. 示例:在程序中引入一个第三方包中的类:`org.apache.commons.langs.StringUtils`
  5. 如果使用 _Maven_ 进行项目管理,我们首先需要在 _pom_ 文件中引入 [_Apache Commons Lang_](https://mvnrepository.com/artifact/org.apache.commons/commons-lang3) 包的依赖
  6. ```xml
  7. <dependency>
  8. <groupId>org.apache.commons</groupId>
  9. <artifactId>commons-lang3</artifactId>
  10. <version>3.11</version>
  11. </dependency>

然后回到我们的代码中,使用 import 关键字即可引入第三方包

如下所示:

  1. package com.github.hcsp;
  2. import org.apache.commons.lang3.StringUtils;
  3. public class Main {
  4. public static void main(String[] args) {
  5. System.out.println("Empty string is empty: " + StringUtils.isEmpty(""));
  6. }
  7. }

程序输入结果:

  1. Empty string is empty: true

我们发现,上述程序,无论是 String 还是 System 类都没有通过 import 和书写全限定类名,而是直接使用。

那是因为 StringSystem 类放在 java.lang 包下。

Java 语言规定:如果一个类放在 java.lang 包下,我们就可以不用写 import 和全限定类名,而是直接使用。

五:方法,静态方法与静态成员变量

main 方法

Java 程序执行的入口是 main 方法

程序示例:

  1. package com.github.hcsp;
  2. public class Main {
  3. public static void main(String[] args) {
  4. }
  5. }

main 方法签名:

  • public 修饰符:public 代表公开的类,没有限制可以自由调用
  • static 修饰符:static 代表静态的,用 static 修饰的方法和变量不和任何对象绑定,代表我们不用创建任何对象就可以调用
  • void:说明该方法没有返回值
  • String[] args:传递给 main 方法的命令行参数,表示为字符串数组

静态方法与静态成员变量

程序示例一:

  1. package com.github.hcsp;
  2. public class Main {
  3. public static void main(String[] args) {
  4. int i = 0;
  5. add(i);
  6. add(i);
  7. add(i);
  8. System.out.println(i);
  9. }
  10. public static void add(int i){
  11. i++;
  12. }
  13. }

程序输出结果为:

0

原因在于,add 方法中传递的参数 i 仅作用在 add 方法块内,所以无法对 main 方法内的变量 i 产生任何影响。

程序示例二:

package com.github.hcsp;

public class Main {
  public static int i = 0;

  public static void main(String[] args) {
    add();
    add();
    add();
  }

  public static void add() {
    i++;
  }
}

该程序运行的结果为:

3

static 修饰的方法或成员变量都独立于该类的任何对象,或是说不依赖于任何对象,它是存在于 JVM 中的一块内存,是一个全局的存储单元,可以被所有对象所共享。所以 add 方法会对其产生影响。

六:对象,构造器与成员变量

Java 是一个面向对象的语言。

类是一种抽象的概念,对象则是类的实例,是一种具体的概念。

示例:创建一个对象

Cat

package com.github.hcsp;

public class Cat {
  private String name;

  public Cat(){
  }

  public Cat(String name) {
    this.name = name;
  }
}

Main

package com.github.hcsp;

public class Main {
  public static void main(String[] args) {
    Cat cat = new Cat("Tom");
  }
}

创建对象最简单的一种方式就是:使用 new 关键字

在本示例中,我们创建了一个名字叫 TomCat 对象,调用了有参的构造器

如果我们不在 Cat 类中声明任何构造器,那么编译器会自动为我们声明一个无参的构造器;相反,如果我们声明了任何有参的构造器,编译器都不会再为我们自动声明这个无参的构造器了,需要我们自己进行声明。

七:实例方法与空指针异常

示例程序:

Cat

package com.github.hcsp;

public class Cat {
  private String name;

  public Cat() {

  }

  public Cat(String name) {
    this.name = name;
  }

  public void meow() {
    System.out.println("喵,我是 " + name);
  }
}

Main

package com.github.hcsp;

public class Main {
  public static void main(String[] args) {
    Cat cat1 = new Cat("Tom");
    Cat cat2 = new Cat("Harry");

    cat1.meow();
    cat2.meow();
  }
}

程序输出结果:

喵,我是 Tom
喵,我是 Harry

我们接下来看这个程序:

Cat

package com.github.hcsp;

public class Cat {
    private String name;

    public Cat() {

    }

    public Cat(String name) {
        this.name = name;
    }

    public void meow() {
        System.out.println("喵,我是 " + name + ", 我的名字的长度是:" + name.length());
    }
}
package com.github.hcsp;

public class Main {
  public static void main(String[] args) {
    Cat cat1 = new Cat();
    Cat cat2 = new Cat("Tom");

    cat1.meow();
    cat2.meow();
  }
}

运行程序:

Exception in thread "main" java.lang.NullPointerException
    at com.github.hcspTest.Cat.meow(Cat.java:15)
    at com.github.hcspTest.Main.main(Main.java:8)

我们会发现,该程序运行出现了异常,这个异常是 NullPointerException 即:空指针异常

原因在于 cat1namenull,对于一个空的对象,我们调用这个对象的方法时,就会产生空指针异常。

规避空指针的方法很简单,我们在可能会产生空指针的地方加入判空的逻辑处理即可:

public void meow(){
  if(name == null){
    System.out.println("我还没有名字!");
  }else {
    System.out.println("喵,我是 " + name + ", 我的名字的长度是:" + name.length());
  }
}

八:对象与引用详解

引用(Reference

举个例子:

A a = new A();

a 就是引用,它指向了一个 A 对象。我们通过操作 a 这个引用来间接地操作它指向的对象。

示例程序:

Cat

package com.github.hcsp;

public class Cat {
    public String name;

    public Cat() {

    }

    public Cat(String name) {
        this.name = name;
    }

    public void meow() {
        System.out.println("喵,我是 " + name + ", 我的名字的长度是:" + name.length());
    }
}

Home

package com.github.hcsp;

public class Home {
    Cat cat;

    public static void main(String[] args) {
        Home home = new Home();
        Cat mimi = new Cat();
        home.cat = mimi;
        mimi.name = "mimi";
    }
}

该程序的内存图分析如下:

1、Java程序的基本结构 - 图3

深拷贝与浅拷贝

浅拷贝和深拷贝最根本的区别就是,拷贝出的东西是否是一个对象的复制实体,而不是引用。

举个例子来形容下:

假设B是A的一个拷贝

在我们修改A的时候,如果B也跟着发生了变化,那么就是浅拷贝,说明修改的是堆内存中的同一个值;

在我们修改A的时候,如果B没有发生改变,那么就是深拷贝,说明修改的是堆内存中不同的值

实现Cloneable接口,重写clone()方法并调用,我们获得的是一个对象的浅拷贝,如示例程序:

Cat

package com.github.hcsp;

public class Cat implements Cloneable {
    public String name;

    public Cat() {

    }

    public Cat(String name) {
        this.name = name;
    }

    @Override
    protected Object clone() {
        Cat cat = null;
        try {
            cat = (Cat) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return cat;
    }
}

Main

package com.github.hcsp;

public class Main {
    public static void main(String[] args) {
        Cat cat = new Cat();
        Cat newCat = (Cat) cat.clone(); // clone方法为浅拷贝
    }
}

那么如何实现深拷贝呢?

深拷贝示例程序:

Cat

package com.github.hcsp;

public class Cat {
    public String name;

    public Cat() {
    }

    public Cat(String name) {
        this.name = name;
    }
}

Home

package com.github.hcsp;

public class Home {
    Cat cat;
}

DeepCopy

public class DeepCopy {
    public static void main(String[] args) {
        Home home = new Home();
        Cat cat = new Cat();
        cat.name = "mimi";
        home.cat = cat;

        Home newHome = deepCopy(home);
    }

    public static Home deepCopy(Home home) {
        Home newHome = new Home();
        Cat newCat = new Cat();
        String newName = new String(home.cat.name);

        newHome.cat = newCat;
        newCat.name = newName;

        return newHome;
    }
}

这样就可以实现一个深拷贝

九:方法的传值 vs 传引用

我们先来看两个程序

程序一:

package com.github.hcsp;

public class Main {
  public static void main(String[] args) {
    int i = 0;
    addOne(i);
    System.out.println(i);
  }

  static void addOne(int i) {
    i++;
  }
}

该程序输出的结果为:

0

因为 addOne 方法中传递的 i 只是 main 方法中的 i 的值的拷贝,所以不会对其产生任何影响。在执行完 addOne 方法以后,该方法空间会被销毁。

程序二:

package com.github.hcsp;

public class Main {
  public static void main(String[] args) {
    Cat cat = new Cat();
    cat.name = "haha";

    renameCat(cat);

    System.out.println(cat.name);
  }

  static void renameCat(Cat cat){
    cat.name = "mimi";
  }
}

该程序运行的结果为:

mimi

为什该程序就会改变 cat 的名字呢?因为方法中传递的是 Cat 变量引用(地址)的拷贝,所以,在 rename 方法中的 cat 指向的也是内存中同一只 “猫”。

Java 中有两种数据类型:

  • 原生数据类型
    • int
    • char
    • byte
    • boolean
    • float
    • double
    • short
    • long
  • 引用数据类型

十:值传递与引用传递

我们通过上面的程序简单了解了什么叫 “值”,什么叫 “引用”,接下来我们重点介绍一下值传递与引用传递。

请不要忽视这个问题,它也许没有你想的那么简单。绝大部分初学者很难搞懂究竟什么是值传递,什么是引用传递。
很多博客中,作者不仅没有解释清楚 “值传递” 与 “引用传递”,还混淆了很多错误的引导。

所以,在回答这个问题之前,我们先进行一次排雷检查,看一看你是否对值传递与引用传递有着错误的理解:

  • 【观点 1】Java 中既有值传递也有引用传递
  • 【观点 2】Java 中只有值传递,因为引用的本质就是指向堆区的一个地址,也是一个值。

如果你的观点符合上述两种观点的其中一种,那么你多半没有理解值传递和引用传递的概念。话不多说,接下来我来详细说明究竟什么是值传递,什么是引用传递,以及 Java 中为什么只有值传递。

值传递(Pass by value)与引用传递(Pass by reference)属于函数调用时,参数的求值策略(Evaluation Strategy)。求值策略的关注点在于,求值的时间以及传值方式:

求值策略 求值时间 传值方式
Pass by value 函数调用前 原值的副本
Pass by reference 函数调用前 原值(原始对象)

所以,区别值传递与引用传递的实质并不是传递的类型是值还是引用,而是传值方式,传递的是原值还是原值的副本。

如果传递的是原值(原对象),就是引用传递;如果传递的是一个副本(拷贝),就是值传递。再次强调一遍,值传递和引用传递的区别在于传值方式,和你传递的类型是值还是引用没有一毛钱关系!

Java 语言之所以只有值传递,是因为:传递的类型无论是值类型还是引用类型,Java 都会在调用栈上创建一个副本,不同的是,对于值类型而言,这个副本就是整个原始值的复制;而对于引用类型而言,由于引用类型的实例存储在堆中,在栈上只有它的一个引用,指向堆的实例,其副本也只是这个引用的复制,而不是整个原始对象的复制。

我们通过两个程序来理解下:

程序一:

public class Test {
    public static void setNum1(int num){
        num = 1;
    }
    public static void main(String[] args) {
        int a = 2;
        setNum1(a);
        System.out.println(a);
    }
}

程序二:

public class Test2 {
    public static void setArr1(int[] arr){
        Arrays.fill(arr,1);
    }
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5};
        setArr1(arr);
        System.out.println(Arrays.toString(arr));
    }
}

程序一输出的结果为:2
程序二输出的结果为:[1,1,1,1,1]

程序一中,Java 会将原值复制一份放在栈区,并将这个拷贝传递到方法参数中,方法里面仅仅是对这个拷贝进行了修改,并没有影响到原值,所以程序一的输出结果为 2。

image.png

程序二中,Java 会将引用的地址复制一份放在栈区,复制的拷贝和原始引用都指向堆区的同一个对象。方法通过拷贝地址找到堆区的实例,对堆区的实例进行修改,而此时,原始引用仍然指向着堆区的实例,程序二的输出结果为:[1,1,1,1,1]

image.png

所以,在 Java 中,对于方法的参数传递,无论是原生数据类型,还是引用数据类型,本质上是一样的。

如果是传值,那就将值复制一份,如果是传引用(地址),就将引用(地址)复制一份。因为传入方法中的都是原始值的复制,所以说,Java 中只有 “值传递”。

Java 中,对于方法的参数传递,无论是原生数据类型,还是引用数据类型,本质上是一样的,如果是传值,那就将值复制一份,如果是传引用(地址),就将引用(地址)复制一份。这一点大家一定要牢记。