关于系列文章,我已经移至社区文档,不会在博客相关内容中更新了,望大家知悉,欢迎订阅文档《爪哇笔记》~
欢迎阅读原文:www.yuque.com/dobbykim/java-basic/...
一: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)加以区分。
package com.github.hcsp;
public class Home {
com.github.hcsp.pet1.Cat cat1;
com.github.hcsp.pet2.Cat cat2;
}
- 包限定了访问权限
四:在Java中引入第三方包
示例:在程序中引入一个第三方包中的类:org.apache.commons.langs.StringUtils
。
如果使用 Maven 进行项目管理,我们首先需要在 pom 文件中引入 Apache Commons Lang 包的依赖
<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 指向的也是内存中同一只 “猫”。
- 什么是值传递? 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
- 什么是引用传递? 引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
Java 中有两种数据类型:
- 原生数据类型
- int
- char
- byte
- boolean
- float
- double
- short
- long
- 引用数据类型
在 Java 中,对于方法的参数传递,无论是原生数据类型,还是引用数据类型,本质上是一样的。
如果是传值,那就将值复制一份,如果是传引用(地址),就将引用(地址)复制一份。
所以,对于基本类型,Java 会将数值直接复制一份并传递到方法中,所以,方法里面仅仅是对复制后的数值进行修改,并没有影响到原数值;对于一个引用类型,Java 会将引用的地址复制一份,把它当作值传递到方法中,方法中传递的是指向堆内存中的那个地址,等同于对堆内存的同一对象进行操作,所以会改变对象的信息。