1.Java 程序的基本结构

Java
462
0
0
2022-04-27

关于系列文章,我已经移至社区文档,不会在博客相关内容中更新了,望大家知悉,欢迎订阅文档《爪哇笔记》~

欢迎阅读原文: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 的字节码。如果想要执行字节码文件,平台上面必须安装 JVMJVM 解释器会将字节码解释成依赖于平台的机器码。

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

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

类(class)

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

一个最简单的类

public class Cat {
}

包(package)

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

示例:

一个属于my.cute 包下的 Cat

package my.cute;
public class Cat {
}

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

例如:

package com.alibaba.fastjson;

这样可以避免类名冲突。

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

包的意义与作用:

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

四:在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 和书写全限定类名,而是直接使用。

那是因为 StringSystem 类放在 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 关键字

在本示例中,我们创建了一个名字叫 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";
}
}

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

深拷贝与浅拷贝

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

举个例子来形容下:

假设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 指向的也是内存中同一只 “猫”。

  1. 什么是值传递? 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
  2. 什么是引用传递? 引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

Java 中有两种数据类型:

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

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

如果是传值,那就将值复制一份,如果是传引用(地址),就将引用(地址)复制一份。

所以,对于基本类型,Java 会将数值直接复制一份并传递到方法中,所以,方法里面仅仅是对复制后的数值进行修改,并没有影响到原数值;对于一个引用类型,Java 会将引用的地址复制一份,把它当作值传递到方法中,方法中传递的是指向堆内存中的那个地址,等同于对堆内存的同一对象进行操作,所以会改变对象的信息。