类中的方法

在方法中使用变量和属性

在 Java 类中的方法中,我们可以直接声明各种变量使用,但是如果需要使用这个类中的属性的时候,有两种方法可以调用这个属性。一种是使用 this.field 这个this 始终指向当前实例。另外一种就是比较简单的,如果不存在属性和变量名冲突的情况下,可以直接像调用变量一样调用这个类的属性(局部变量的优先级大于类属性)

方法也想要可变参数

一个类的方法可以设置输入的参数,调用的时候就严格的按照设置的参数进行输入就可以了,但是如果不确定调用方法的时候要输入的参数的时候该怎么办呢?这里就要说到可变参数了。

可变参数的设置方法就是 Type... variable 然后调用这个方法的时候,可以传入任意数量的参数,Java会把这些参数整合成为对应类型的数组。需要注意的是,这个可变参数必须在所有参数的最后面,否则就会报错。

下面就是两种实现可变参数传入的方法,第一种是传进去之后Java整合成数组,第二种是构造好一个数组之后再传进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class test
{
private static void Tempest(int Num,String... names)
{
for(String name:names)
System.out.println(name);
}
private static void Xorex(String[] names)
{
for(String name:names)
System.out.println(name);
}
public static void main(String[] args)
{
String[] names={"Tempest","Xorex"};
Xorex(names);
Tempest(2,"Xorex","Tempest");

}
}

实参是怎样传进去的

对于传入的基本类型的变量,是通过复制传入的,也就是说调用方法传入参数的时候,实际上传入的是一个数据,无论这个数据被形参接收之后经过怎样的更改,也影响不到实参。这点和 C 语言是一样的。

对于传入的是引用类型的呢,传入的其实是引用的坐标,如果方法内部的形参更改了其应用的数据,外部的实参同样引用的是这个数据,所以就会发生更改。这里就想 C 语言的指针一样,传进去的是地址。

方法签名

那么 Java 是如何识别一个特别的方法呢?其实是通过方法签名实现的,所谓方法签名,其实就是 方法名称以及方法形参 的集合,如果两个方法签名相同,也就是方法名称和方法形参都相同,那么这就是一个方法,否则就是两个不同的方法。(注意方法签名是不包括返回值类型的!)

构造方法

为了实现在创建一个实例的时候,对类中的属性进行初始化,所以Java有自己的构造方法,作用和PHP的 __construct() 魔术方法相同,但是细节不太一样。

构造方法的格式是:

1
2
3
4
public ClassName(Type Paramater)
{
//初始化语句或者其他语句
}

这里需要注意的是构造方法没有返回值,甚至连 void 也没有,其方法名称必须是类的名称,然后里面是在 new 一个类的时候,需要传入的参数。

我们可以创建多个构造方法,来对应可能的各种数据输入,有些类在初始化的时候输入的参数格式有好几种,我们就可以写多个构造方法来对应不同的数据输入,程序会自动判断输入数据的格式来决定调用不同的构造方法。

多个构造方法之间可以相互调用,具体方法就是在构造方法中使用 this() 方法,里面填充所所需要调用构造方法的参数的格式就可以匹配到了。

方法重载

在构造方法的时候,其实就已经使用了构造方法的重载(Overload),我们创建多个构造方法,对应不同的形参,来处理不同的实参输入。同理,所有其他的方法也可以使用这样的方式(声明多个不同形参的同名方法)进行方法重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class test
{
public static void main(String[] args)
{
Method Type=new Method();
System.out.println(Type.GetType("Xorex"));
System.out.println(Type.GetType(0.1));
System.out.println(Type.GetType(23333));
}
}

class Method
{
public String GetType(String string)
{
return "String";
}
public String GetType(int num)
{
return "int";
}
public String GetType(double num)
{
return "double";
}
}

上面的代码就是利用方法重载来实现对于不同的实参输入,调用对应的方法来实现特定的功能,整个过程中调用的方法名字都是相同的。

类的继承

继承语法

类的继承就是获得另外一个更加基础的类的属性和方法,然后在这个基础上增加其他的特殊方法和属性,这样可以大大的减少代码数量,保证准确性。

继承的语法是:class Son extends Father

如果子类中定义了和父类名称相同的属性或者方法的时候,在外部调用是优先调用子类中的属性或者方法,即使子类的前缀是 private 而父类的前缀是 public ,仍然会调用子类的,然后报错。

继承树

所有的类都继承自其他的类,如果自己在定义类的时候没有写 extends ,那么就会默认继承 Object 类(有Python继承链的感觉了)

至于这个 Object 类以后会详细了解它的。

protected关键词

这和 public private 一样是一个作用域的关键词,因为有些子类会使用父类的一些属性和方法,但是又不想破坏封装性,则可以使用 protected 关键词,这个关键词使得此属性和方法只能在本类和子类中使用,在外部无法调用。

super关键词

super 和 this 的作用是一样的。this 用来特指是当前类的属性或者方法,用来防止和方法内部声明的同名变量搞混。super 同样是为了特指是父类的属性或者方法,用来防止和子类的搞混。

对于一个属性的优先级来说,同名的变量,子属性,副属性,优先级是递减的,使用他们共同的名字所实际调用的内容取决于所存在的所有同名者中优先级最高的那个,所以为了方便的调用我们想要的数据,最好加上诸如 super this 这样的修饰符加以区分。

super还有一个作用,那就是当父类有构造方法的时候,super() 函数就是父类的构造方法调用途径,而且这个 super() 必须在子类的构造方法中的第一行被调用,并传入对应的参数,除非父类没有构造函数或者构造函数不需要传入参数,否则就无法通过编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class test
{
public static void main(String[] args)
{
Two a=new Two(1,2);
}
}
class One
{
public One(int a,int b)
{
System.out.println(a+b);
}
}

class Two extends One
{
public Two(int a,int b)
{
super(a,b);
}
}

上面的代码演示的就是 super() 的作用,用于父类的构造方法的传参。

阻止继承

Java中有一个叫作 final 的修饰符字符,加在 class 关键词前面,意思就是禁止任何类从它这里继承。

类之间的转型

类之间也可以进行类型转换,但是只可以从子类转化为父类,不可以从父类转化为子类,因为子类比父类多了很多东西,这些没有办法还原。

在类型转换的时候,为了避免发生将父类转化为子类,可以使用二元操作符 instanceof 来判断一个实例是否是另外一个类的实例或者子类的实例,如果是,那么这个实例就可以被转化为判断的类,用法:

1
2
3
4
5
a=new Student();
if(a instanceof Person)
{
Person b=(Person)a;
}

这样只有确定实例 a 的类 Student 为 Person 的子类之后,才执行下面的类型转换,防止出现异常。

关于继承和组合

在程序设计的时候,我们更多的是需要考虑面对对象的合理性,比如因为 Student is Person 所以我们可以让 Student 继承于 Person 里面的属性和方法,但是对于另外一个类 Book,我们只能说 Student has Book,这应该是拥有或者说组合的关系,所以 Book 是 Student 的属性,而不能用于继承。

类的多态

要说起多态,就不得不讲述一下 重写(override),重写是对所继承的父类的方法,进行再次定义。被识别为重写的要求是子类定义的方法的方法签名和父类的方法的方法签名相同,也就是拥有相同的方法名和形参列表。

这样这个新定义的方法因为优先权高于父类的方法,那么调用的时候就会优先调用子类中新定义的方法,相当于父类的方法被重写了。

其次需要讲述的是子类到父类的类型转化,也就是将一个内容更多的子类重新压缩成父类,这个时候重写的方法就会真正的覆盖父类的方法(因为在压缩的时候,调用优先权高于父类的方法,所以被压缩进去的是子类重写的新方法),这样就使得虽然一个实例的类型被压缩成了父类,但是这个父类可以有很多种状态(因为可以被各种子类重写方法),所以就有了类的多态,你无法确定一个类的具体状态,除非它真正运行到这一步。

我们就可以利用类的多态干很多事情了,尤其是要维护很多代码的时候,我们可以不更改老代码,通过重写方法来完成各种新功能。

当然在子类中,如果想要调用优先级不够高的父类中的相同签名的方法,可以使用 super.Method() 这样的方式调用。

final修饰词

如果不想类被继承,可以使用 final 修饰词在类名前面来拒绝继承。

如果方法不想被重写,可以使用 final 修饰词在方法名前面来决绝重写。

如果属性不想被修改,同样可以使用 final 修饰词在属性名前面来拒绝修改。

抽象类

类中有一个修饰词,叫作 abstract 抽象的,被这个修饰词修饰的类,被称为抽象类。在拥有了定义没有语句的模板方法的能力的同时,作为代价,丧失被实例化的能力。

正是因为类拥有多态,所以也正是因此诞生了抽象类,也就是专门用来实现多态父类。 抽象类可以定义任意个抽象方法,所有的抽象方法都没有执行语句,只有一个方法的签名,用来被子类重写。

所有继承于抽象类的子类,必须按照抽象类里面所定义的所有抽象方法的签名,对其进行重写。抽象方法的作用就是提供一个重写的模板,保证所有的子类都拥有并且定制化这些功能,这样就可以标准化类的多态。

抽象类可以没有抽象方法,但是有抽象方法的必须是抽象类。

接口 Interface

什么是接口呢,就是一个只有抽象方法的类,没错,只有抽象方法,甚至连字段都没有的类。这个东西就被成为接口。

定义接口的方式就是 interface ClassName ,然后里面疯狂定义方法就可以了,因为接口里面全都是抽象方法,所以关键词 abstract 就可以省略了,因为所有的抽象方法都是 public 类型的,所以修饰词 public 也可以省略。

除了抽象方法,接口中还有一种方法叫作 默认方法。如果我们想要在接口中加入一个新的抽象方法,那么所有接入这个接口的类都需要更改代码了,这也太麻烦了。于是就在接口中引入了一个默认方法,默认方法可以不是抽象方法,里面可以定义语句,允许接入这个接口的类不重写这个方法,然后让特定需要这个新方法的类重写方法即可。

虽说表现上和抽象类中的普通方法相似,可以重写也可以不重写,但是要知道接口里面是没有属性的啊!抽象类里面还可以有属性来玩,接口里面啥都没有,只需要在函数修饰词前面加上 default 就可以了。

普通的类如何接入(实现)接口呢?当然是使用 class ClassName implements InterfaceName 来接入(实现)接口,一个类可以接入多个接口,直接用逗号隔开接口名就可以了。(implements 实现)

最后注意这个接口指的是 Java 里面狭义的接口,广义上的接口有很多意思。

静态字段和静态方法

所谓静态,其实就相当于类本身的全局变量,只要类声明出来,静态字段和静态方法就存在了,不需要实例化就能访问和调用。静态的东西是类本身的一部分,所以是通过类名调用的,而不是通过实例的名字。

下面的代码就是利用静态字段和静态方法,通过类名来引用,从而描述类的一些性质(被实例化的次数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class test {
static public void main(String[] args) {
Xorex a=new Xorex();
Xorex b=new Xorex();
Xorex c=new Xorex();
System.out.println(Xorex.GetTimes());
}
}

class Xorex {
public static int Times=0;
public static int GetTimes() {
return Times;
}
}

一般来说,静态字段和静态方法一般用于探明这个类的本身的时候使用,比如返回类被实例化了多少个,类自己本身的属性,等等。

比较特殊的是,接口不能有普通的字段和方法,但是可以有静态的字段和静态的方法,这些可以用来专门对接口本身进行描述和作用。需要注意的时这个接口的静态字段的类型只能是 public static final 类型(也就是前面的修饰词可以省略啊),所以无论是外部还是内部都无法改变接口的字段。

具体这些抽象类和接口的静态方法有什么巧妙的使用,这些以后会慢慢分析。

导入 imort PackageName

Java 可以把一些代码打包,来提供给其他的程序调用,如果需要调用一个包,那么调用方法就是 import PackageName ,其中 PackageName 是需要有目录顺序的,比如在根目录下面有一个文件夹叫作 Tempest,文件夹里面有一个文件 Xorex.class ,那么在导入这个包的时候,就需要写:import Tempest.Xorex 在import 这里,. 会被解读为路径的分隔符。

这个 import 写带分隔符的全名就是告诉 JVM 去哪里找我们从外部调用的这个类,当然想要调用一个类不写 import 也可以,这不过需要在每次调用这个类的时候,都写上包的全名(包括路径)。

所以,最好还是用 import 直接把要调用的包的路径写出来,这样每次调用直接写类名就可以了。

声明 package PackageName

声明当前的 Java 文件所属于的包的方法就是在文件的开头加上一句话:package PackageName 其中,需要注意的是这个 Java 文件需要在一个名字为 PackageName 的文件夹下面,也就是说,所有在 PackageName 文件夹下面的 Java 文件都应该属于 PackageName 这个包中。

在我们为类写属性和方法的时候,我们往往会给这些属性和方法一个修饰词如 public/private/protected,这些都是作用域的修饰词,表示的是这些属性和方法能够被哪些作用域范围内生成的实例给调用。比如下面的表:

作用域 当前类 同一package 子孙类 其他package
public
protected ×
private × × ×

当我们引入 Package 的时候,我们有了一个全新的空间概念:包作用域。同一个包下面类虽然有可能不是属于一个 java 文件,但是他们都属于同一个包中,所有包中的类的关系都可以看成 friend 朋友。朋友之间都是平等的。同一个包中的所有类的关系和同一个文件中所有类的关系是等价的,因为他们都属于一个包中。(下面的表中 friendly 指的是没有修饰符的属性和方法的作用域)

作用域 当前类 同一package 子孙类 其他package
public
protected ×
friendly × ×
private × × ×

需要注意的是,上面的关系中,子孙类一栏中的调用,既包含了实例化的调用,也包含了继承化的调用。

emmmm,其实只需要记住,类在同一个包里面的关系等价于在同一个文件里的关系就可以了。

JVM 如何寻找类

对于 Java 中实例化一个类的时候,JVM 会按照一定的顺序去寻找这个类的代码,探究寻找顺序可以帮我们解决在实例化同名的类的时候,究竟是哪一个类被实例化。

  • 首先查找当前 package 是否存在这个 class
  • 其次查找 import 的包是否包含这个 class
  • 最后查找 java.lang 包是否包含这个 class

类的修饰符

类的修饰符很少,除了不加修饰符的情况以外,只有三种,分别是: public abstract final

  • 不加修饰符的时候,表示这个类属于默认的访问模式,也就是只能被同一个包中的其他类给访问。

  • 使用 public 修饰符的时候,表示这个类的被访问模式就不仅仅局限与同一个包里的类了,而是所用使用 import 或者完整路径的地方都可以访问它。换句话说,任何地方都可以访问这个类。需要注意的是,任何一个 Java 文件中都只能有一个 public 类,并且这个 public 类的名称必须和此 Java 文件的文件名称相同,作为这个 Java 文件可被外界访问的入口。public 类一般都是造轮子用的类,集成一些特定的功能来供外界使用。

  • 使用 abstract 修饰符,表示这个类里面含有抽象方法,需要子类重写这个抽象方法,并且抽象类失去被实例化的能力。

  • 使用 final 修饰符,表示这个类的形态是最终状态,被禁止通过继承来改写功能。

内部类

除了在 Java 文件里面到处声明类以外,还可以在一个类的内部声明一个类,没错就是套娃,套娃的方式有两种,一种是按照普通方式直接在类里定义一个类,另外一种是在类的方法里面定义一个匿名类。

内部类有一个很重要的特性就是可以访问外部类的 private 属性和方法。

定义内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class test {
public static void main(String[] args) {
Outer outer=new Outer();
Outer.Inter inter=outer.new Inter();
inter.GetString();
}
}
class Outer {
class Inter {
void GetString() {
System.out.println("This is Inter Class!");
}
}
}

上面的代码就是在类 Outer 里面定义了一个内部类 Inter,定义内部类的方法很简单,就是在外部类的大括号里面像定义一个普通类一样就可以了。

但是实例化内部类 Inter 就需要借助外类 Outer 了,首先需要先将外类 Outer 实例化出来,声明内部类的类型 Outer.Inter ,然后借助外类的实例 outer,进行新建(new) Inter。代码: Outer.Inter inter=outer.new Inter();

内部类的类型:Outer.Inter

借助外部类实例新建内部类:outer.new Inter()

定义匿名类

匿名实质上就是一个没有名字,用来继承父类或者实现接口的子类,但是这个临时的子类可以在方法里面进行使用。

首先需要一个用来继承的父类或者接口,在实例化的同时,在后面添加大括号,然后就可以进行添加属性方法,重写方法等等操作了,比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package JavaProject.Test;

public class test {
public static void main(String[] args) {
Outer outer=new Outer();
outer.GetInter1();
outer.GetInter2();
}
}

class Tempest {
int x;
void GetString()
{
System.out.println("GG");
}
}
class Outer {
void GetInter1() {
Tempest One=new Tempest() {
@Override
void GetString() {
Tempest Two=new Tempest() {
@Override
void GetString() {
Tempest Three=new Tempest() {
@Override
void GetString() {
System.out.println("One in One!");
}
};
Three.GetString();
}
};
Two.GetString();
}
};
One.GetString();
}

void GetInter2() {
new Tempest() {
void GetString() {
System.out.println("Xorex");
}
}.GetString();
}
}

其中 GetInter1() 用一个父类来压缩了继承自他的匿名类,这个匿名类重写了父类的方法,重写一个一次性的子类实现父类的多态。

GetInter2() 没有将子类压缩成父类,而是对父类进行了拓展/重写,但是调用这个匿名子类也只有在这里实例化的时候可以,也是一次性使用的。

static 内部类

我们在第一种内部类的前面加上了 static 之后,这个类就脱离了对外类的依赖变成了一个独立的类,也就是可以不依赖外部类的实例化而调用内部类,只不过这个内部类的名字需要带上外部类作为标识,比如可以直接这样实例化内部类:Outer.Inter Xorex Outer.Inter();

classpath 和 jar

classpath

在 Java 文件在运行的时候,最后一步是 JVM 执行各种已经变成字节码的 .class 文件。从入口 .class 文件开始,这个文件往往会使用其他的各种类,有自己写的,有从其他人手里下载的,有 Java 核心库里面的。那么 JVM 如何寻找这些被开发者调用的类文件呢?

答案就是利用 classpath ,这个是一个环境变量,用来记录存放 .class 文件的地方,如果存在一个 xorex.class 位于 E:/JavaProject/bin/tempest/xorex.class 那么我们可以将 E:/JavaProject/ 加入到 classpath 环境变量里面,这样我们只需要用 bin.tempest.xorex 来调用xorex 类,JVM 就可以寻找到它并执行。

但是不推荐将 classpath 添加到环境变量里面,因为这会污染整个系统环境,应该在运行程序的时候就直接在对应的命令里面写入 classpath。

不过现在比较少用到这个东西了,JVM 会自动加载当前路径和 Java 核心库,这已经能应对很多情况了(可能?)

jar

jar 格式是什么?

jar 的全名是 Java Archive,Archive: [ˈɑːrkaɪv] 存档、档案馆 。

jar.exe 的作用就是把 .class 文件保留原本的目录,打包成一个文件。jar 文件和 zip 文件本质上是一样的(都是一个压缩包),只不过 jar 文件会多一个 META-INF 文件夹和 MANIFEST.MF 文件。这个文件里面的配置信息是让 jar 文件可以在 JVM 上运行并让其变成一个 java 应用程序的关键。

jar 文件的作用就是封装发布,它既可以是当作轮子库来供其他开发者使用,也可以作为一个可以直接运行的 java 应用程序供消费者使用。比如 java 的标准库就叫做 rt.jar 大概有 60M 的大小,它被默认的添加到了 JVM 的 classpath 里面。

jar 只是用来存放 .class 文件的容器。

来创建 jar 文件吧

生成 jar 文件可以使用 jar.exe 的相关命令

首先先看命令格式:jar {ctxui} [vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files

其中大括号里面(有一个即可)和文件名是必须要有的参数,中括号为可选参数。

  • -c:创建一个jar包
  • -t:显示jar中的内容列表
  • -x:解压jar包
  • -u:添加文件到jar包中
  • -f:指定jar包的文件名
  • -v:生成详细的报造,并输出至标准设备
  • -e:为捆绑到可执行jar文件的独立应用程序指定应用程序入口点
  • -m:指定manifest.mf文件.(manifest.mf文件中可以对jar包及其中的内容作一些一设置)
  • -0:产生jar包时不对其中的内容进行压缩处理
  • -P:保留文件名中的前导 ‘/‘ (绝对路径) 和 “..” (父目录) 组件
  • -M:不创建条目的清单文件(Manifest.mf)。这个参数与忽略掉-m参数的设置
  • -i:为指定的jar文件创建索引文件
  • -C:表示转到相应的目录下执行jar命令,相当于cd到那个目录,然后不带-C执行jar命令
  1. 创建 jar 文件

jar cvf test.jar test 命令表示使用 jar.exe 程序将 test 文件夹下面的东西打包成 test.jar 文件。

  1. 查看 jar 文件内容列表

jar tvf test.jar 表示查看 test.jar 包里面的内容列表。如果内容过多,我们可以将列表内容导入到一个文件里面 jar tvf test.jar > list.txt 这样就把内容列表以文本的方式保存在了 list.txt 文件里面。

  1. 解压 jar 文件

jar xvf test.jar 表示解压 test.jar 文件到当前的目录下面。

  1. 更新 jar 文件

jar uvf test.jar Test/Test.class 表示将 Text目录下面的 Test.class 文件加入到 test.jar 里面,加入 jar 里面的同时,会保留 .class 文件的目录,也就是在 jar 的 Test 目录里新加入 Test.class

模块

模块是什么东西呢?模块就是被封装的 jar 文件。

如果翻阅一下 java8 和 Java9 之间的不同,你就会发现 java8里面的 jar 文件都消失了,而到了 java9 里面全部变成了 jmod 文件(java module)

jmod 其实就是对 jar 文件的一个改进,因为 jar 文件里面的各种类没有规定访问权限,也没有规定依赖关系,这就导致很难管理它们。比如一个 rt.jar 就足足60MB 的大小,如果要调用里面的工具类使用的话,光轮子都有 60MB 的大小,太臃肿了,而且里面有很多类是实现某个功能的中间类,并不想被调用(可能会有奇怪的错误)。

所以是时候对 jar 文件来一个规范了,可以单独列出来一个 module descriptor 文件,里面规定了自己的哪些类可以被外界调用,而自身又需要哪模块的哪些些类。这样就可以将一个 jar 文件差分成很多 jmod 文件,然后需要哪些 jmod 就组合哪些,jmod 的描述文件里写有各个模块的依赖关系,所以保证功能的前提下极大的精简了体积。因为可以限制对外界开放的类,所以也大大保证了代码功能的封装性。