类(class)是Java中对一个实体的描述。对于一个实体,我们可以从属性和行为两个角度来描述。比如面对一只狗,它的属性有:姓名、性别等,它的行为有:吃饭、汪汪叫等。
那么在Java中如何体现这两个角度的描述呢?属性我们可以用基本数据类型来表示(在后面的学习中,我们也可以用其他的类来表示)。行为我们可以用方法(就是我们在C语言中说到的函数)来表示。
[public] class 类名{
属性
方法
}
其中,类中的属性叫做“成员属性”,类中的方法叫做“成员方法”。
这里需要解释一个地方,class
前面的public
是可选择的,如果我们当前这个类和该java
文件的文件名相同的时候,该类前面需要加public
关键字,并且每个文件有且仅有一个类前面加public
。比如我们有一个文件叫:test.java
,那么在这个文件中,我们的test
类的前面就需要加上public
,但是其他类的前面不加该关键字。
另外大家不要随便地举一反三,有的读者可能了解到了public
是一个访问限定符,就会有这样一种想法,这个能加public
的类前面能不能将public
替换成protected
或者private
呢?答案是不可以的。这个与文件名同名的类的前面只能加public
。
我们以刚刚的狗为例,看看我们应该如何描述?
public class Dog{
//属性描述
public String name;
public int age;
//行为描述
public void eatFood(){
System.out.println(name + "正在吃饭。");
}
public void barks(){
System.out.println(name + "正在汪汪叫。");
}
}
我们在上文中学到了如何描述一个对象,那么我们如何根据这个描述去实例化一个具体的对象呢?就好像,我们已经知道了狗有哪些属性以及哪些行为,那么我们是不是可以利用这些描述去描述生活中一只“具体”的狗呢?
这种根据抽象的描述创建具体的对象的过程就叫做实例化。
类名 对象名 = new 类名();
我们还用刚刚的狗为例子,实例化过程如下。
Dog dog = new Dog();
那么我们如何访问对象中的具体属性和方法呢?我们可以通过类名.属性名
的语法去访问成员属性,当然也可以通过类名.方法名(参数)
来访问成员方法。
我们创建一个main函数,那么我们可以写:
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "Tom";
dog.eatFood();
dog.barks();
System.out.println("狗的名字是" + dog.name);
}
我们在刚刚的语法中发现了一个很奇怪的地方。我们在new
关键字的后面,写了一个类似于调用方法的代码段Dog()
,这一部分其实就是调用了构造方法
。
构造方法就是在new
对象的同时调用的方法,语法是类名()
。但是我们会发现在之前的代码中,我们并未写构造方法,我们没写,编译器怎么调用呢?
其实,当我们不写的时候,Java会为我们提供一个默认的构造方法,这个构造方法没有参数,方法体内也是空白的,具体如下(以Dog
类为例):
public Dog(){
}
这也就是为什么我们在调用构造方法的时候,没有在括号内写参数。
那么我们能不能根据我们的需要重新写构造函数呢?答案是可以的。语法如下:
public 类名(参数1,参数2...){
方法体
}
我们以刚刚的Dog
类为例子,如果我们想在构造Dog
对象的同时给这个对象赋值。那么我们可以这样写构造方法。
public Dog(String n, int a){
name = n;
age = a;
}
由于我们重写了构造方法,所以原先编译器提供的构造方法就消失了,根据我们刚刚的例子,重写之后,我们有了形参,所以在new
对象的时候就要传进去。
那么就可以写成
Dog dog = new Dog("Tom", 2);
我们发现刚刚写的构造方法中,我们很难通过形参的名了解到它想让我们传入什么数据。那么我们可以把形参写地更清楚一些。
public Dog(String name, int age){
name = name;
age = age;
}
但是根据局部变量优先的原则,等号两边的name
都是形参变量。但实际上,我们是想将形参变量name
赋值给成员变量name
。那么为了达到这一目的,我们就引入了this
关键字,我们可以通过this.成员名
来访问成员变量。
那么我们可以将上面的代码转化为:
public Dog(String name, int age){
this.name = name;
this.age = age;
}
我们直接打印一下new
出来的对象,如下所示:
public static void main(String[] args) {
Dog dog = new Dog("Tom", 2);
System.out.println(dog);
}
通过终端我们可以观察到如下结果:
出乎意料的是,它并没有打印出dog
中的成员属性。那么我来解释一下这段打印。
Dog所在包其实就可以理解为它所在的目录。后面才是重点。在学C语言的时候,我们都知道每一个变量对应一个硬件地址,那么这些地址经过转化后,会形成一串16进制的串,转化之后的这个值叫做哈希值
,也叫做hash值
。
而我们的对象名指向的就是这一串hash值,而不是该对象。
具体存储结构如下:
因此我们把类似于dog
的对象名称作引用
。所以,我们的类又叫做引用类型
。
除此以外,类中的成员方法并不是和成员属性在一起的,而是共同存储在了成员方法区。
我们看下面这个例子:
public class Dog{
public static String type;
public String name;
public int age;
}
那么我们new几个对象:
Dog d1 = new Dog("T1", 1, "Animal");
Dog d2 = new Dog("T2", 2, "Animal");
Dog d3 = new Dog("T3", 3, "Animal");
Dog d4 = new Dog("T4", 4, "Animal");
Dog d5 = new Dog("T5", 5, "Animal");
如果转化为内存的话,就相当于有5个引用分别是d1...d5
,他们分别指向堆中五块不同的区域。但是我们发现成员属性type
的值都是animal
,因此我们没必要存储五份,只需要存储一份即可。
因此,我们可以将这一个属性抽离出来,让多个对象共用一个成员属性。
那么,我们就可以用static
去修饰,此时就可以让多个对象共用一个成员变量。
public class Dog{
public static String type;
public String name;
public int age;
}
那么此时的存储结构就会发生如下变化:
加static之前:
加static之后:
既然所有的类对象共用一个静态变量,也就是说静态变量不依赖于对象。
所以即使我们没有实例化对象,我们依旧能够访问到类中的静态成员。
所以我们可以直接用:类名.静态变量名
来访问,当然用实例化后的对象名.静态变量名
访问也可以,但是不规范。
比如:
public static void main(String[] args) {
Dog.type = "Animal";
}
除了成员变量之前可以加static
关键字,我们的成员方法之前也可以加static
。那么根据刚刚的分析,如果加了static
。那么我们的静态方法也可以在不实例化对象的情况下调用。比如我们的println()
方法。