1.Java 程序的基本结构

Java
426
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 会将引用的地址复制一份,把它当作值传递到方法中,方法中传递的是指向堆内存中的那个地址,等同于对堆内存的同一对象进行操作,所以会改变对象的信息。