一:Java语言的跨平台性与字节码概述
JVM,机器码与字节码
JVM 即: Java Virtual Machine 也就是 Java 虚拟机。
Java 语言有一个特点:平台无关性 。JVM 就是实现这一个特点的关键。
我们知道,软件运行依赖于操作系统(Operating System)。早期的开发者使用的程序语言并不具备良好的移植性,如果想在不同的操作系统平台上面运行功能相同的应用,需要为每一种平台都编写可以被平台识别认知的代码。一般编译器会直接将程序的源代码编译成计算机可以直接执行的 机器码。
Java 语言则具有平台无关性,也就是所谓的 Write Once,Run Anywhere (一次编译,到处运行)。Java 编译器并不是将 Java 源代码编译为由 0,1 序列构成的计算机可直接执行的机器码,而是将其编译为扩展名为 .class 的字节码。如果想要执行字节码文件,平台上面必须安装 JVM,JVM 解释器会将字节码解释成依赖于平台的机器码。
从上图也可以看出,不同操作系统需要安装基于该操作系统的 JVM。JVM 屏蔽了操作系统之间的差异,实现了 Java 语言的跨平台性。
二:Java语言的基本单元——类与包
类(class)
在 Java 语言中,类是最小的基本单元
一个最简单的类
public class Cat {
}
包(package)
为了更好地组织类,Java 语言提供了包的机制,用于区别类名的命名空间。
示例:
一个属于my.cute 包下的 Cat 类
package my.cute;
public class Cat {
}
在 Java 语言中,包一般会用域名的反序来命名。
例如:
package com.alibaba.fastjson;
这样可以避免类名冲突。
三:Java语言的基本结构——包的意义
包的意义与作用:
- 把功能相似或相关的类组织在同一个包中,方便查找与管理
- 同一个包中类名要求不能相同,但是不同包中的类名可以相同;当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包也可以避免类名冲突。
示例:
在不同包下有着相同类名的类,我们可以使用全限定类名(Full Qualified Name)加以区分。 ```java package com.github.hcsp;
public class Home { com.github.hcsp.pet1.Cat cat1; com.github.hcsp.pet2.Cat cat2; }
3. 包限定了访问权限
<a name="b691a954"></a>
### 四:在Java中引入第三方包
示例:在程序中引入一个第三方包中的类:`org.apache.commons.langs.StringUtils`。
如果使用 _Maven_ 进行项目管理,我们首先需要在 _pom_ 文件中引入 [_Apache Commons Lang_](https://mvnrepository.com/artifact/org.apache.commons/commons-lang3) 包的依赖
```xml
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
然后回到我们的代码中,使用 import 关键字即可引入第三方包
如下所示:
package com.github.hcsp;
import org.apache.commons.lang3.StringUtils;
public class Main {
public static void main(String[] args) {
System.out.println("Empty string is empty: " + StringUtils.isEmpty(""));
}
}
程序输入结果:
Empty string is empty: true
我们发现,上述程序,无论是 String 还是 System 类都没有通过 import 和书写全限定类名,而是直接使用。
那是因为 String 和 System 类放在 java.lang 包下。
Java 语言规定:如果一个类放在 java.lang 包下,我们就可以不用写 import 和全限定类名,而是直接使用。
五:方法,静态方法与静态成员变量
main 方法
Java 程序执行的入口是 main 方法
程序示例:
package com.github.hcsp;
public class Main {
public static void main(String[] args) {
}
}
main 方法签名:
- public 修饰符:public 代表公开的类,没有限制可以自由调用
- static 修饰符:static 代表静态的,用 static 修饰的方法和变量不和任何对象绑定,代表我们不用创建任何对象就可以调用
- void:说明该方法没有返回值
- String[] args:传递给 main 方法的命令行参数,表示为字符串数组
静态方法与静态成员变量
程序示例一:
package com.github.hcsp;
public class Main {
public static void main(String[] args) {
int i = 0;
add(i);
add(i);
add(i);
System.out.println(i);
}
public static void add(int i){
i++;
}
}
程序输出结果为:
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 关键字
在本示例中,我们创建了一个名字叫 Tom 的 Cat 对象,调用了有参的构造器。
如果我们不在 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 即:空指针异常
原因在于 cat1 的 name 为 null,对于一个空的对象,我们调用这个对象的方法时,就会产生空指针异常。
规避空指针的方法很简单,我们在可能会产生空指针的地方加入判空的逻辑处理即可:
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";
}
}
该程序的内存图分析如下:
深拷贝与浅拷贝
浅拷贝和深拷贝最根本的区别就是,拷贝出的东西是否是一个对象的复制实体,而不是引用。
举个例子来形容下:
假设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。
程序二中,Java 会将引用的地址复制一份放在栈区,复制的拷贝和原始引用都指向堆区的同一个对象。方法通过拷贝地址找到堆区的实例,对堆区的实例进行修改,而此时,原始引用仍然指向着堆区的实例,程序二的输出结果为:[1,1,1,1,1]
所以,在 Java 中,对于方法的参数传递,无论是原生数据类型,还是引用数据类型,本质上是一样的。
如果是传值,那就将值复制一份,如果是传引用(地址),就将引用(地址)复制一份。因为传入方法中的都是原始值的复制,所以说,Java 中只有 “值传递”。
在 Java 中,对于方法的参数传递,无论是原生数据类型,还是引用数据类型,本质上是一样的,如果是传值,那就将值复制一份,如果是传引用(地址),就将引用(地址)复制一份。这一点大家一定要牢记。