初识Java【4】——类和对象
一、面向对象(Object Oriented)
在编程世界中一直有两种思想:一种是面向过程,另外一种则是面向对象。
如各位读者所知:C语言就是一种面向过程(Procedure Oriented)的语言;而C++,Java,还有非常火爆的Python都是面向对象的语言。
当然,这并不是意味着面向过程比面向对象差。面向过程也有自身的优点,在描述比较简单事件的时候,面向过程能够更好的体现出事件发展的时间脉络,步骤非常清晰。而对于面向对象来说,一切皆对象。关注的是:对象本身,以及:对象之间的交互。
为了方便各位读者理解,这里举一个经典的例子:把大象塞入冰箱。
- 如果是面向过程的话。只需要三步:
(1)打开冰箱
(2)把大象塞入
(3)关上冰箱
用上面这个例子各位读者应该能够很好的理解面向过程了,要理解面向过程就抓住过程的先后顺序。
- 如果是面向对象的话,首先就是抽取出对象。上例涉及到三个对象:大象、人、冰箱。
(1)大象:具有能够被塞入的能力
(2)人:A.人能开关冰箱; B.人具有把大象塞入冰箱的能力
(3)冰箱:A. 冰箱能够被打开 ;B.冰箱具有存储大型事物的能力 ; C.冰箱能够被关闭
这里详细讲讲面向对象。如上所示,我们以及抽取出若干对象了,现在我们要完成 “ 把大象塞入冰箱 ” 这件事就需要 大象、人、冰箱 三者的互相配合。
也就是说:作为一个合格的冰箱,冰箱并不需要知道人能干什么,只需要在被打开的时候打开,需要存大象的时候存上,被关上的时候关上就行。只不过这件事情在三者之间的配合中就被完成了,你要是问冰箱,“冰箱,你干了什么?” 它也只会说,“冰箱,干了一个冰箱能干的事。”
总之,要理解面向对象就要抽取出对象,思考对象之间是如何交互的。
在理解面向对象这种思想之后,我们又如何去利用这种思想编程呢?这里我们引入类的概念。
二、类
1.何为类
类是一种用户定义的引用数据类型,也称类类型。类是对现实生活中一类具有共同特征的事物的抽象。在具体编程中,我们用类去实例化一个对象,也就是用类来描述一个对象。
2.类的定义与初始化
(1)类的定义
class ClassName {
field; // 字段、属性、成员变量
method; // 行为、成员方法
}
类的定义中有以下几点值得注意:
A. 用
class + 类名
的方式去定义一个类,其中 类名需要用大驼峰的方式定义。
B. 一个Java文件中只能有一个public class
。如果这个类 和 主类放在同一个文件夹中,那么就不能使用public
修饰。
为了方便对下三个名词进行解释,笔者编写了Aniaml类
,请各位读者自行阅读。
class Animal {
public String name;
public int age;
public Animal(String name) {
this.age = age;
System.out.println("这有一个动物!");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("这还有一个动物!");
}
public void eat() {
System.out.println("有个动物在吃东西!");
}
}
1)成员变量
在Aniaml类
中name 、age
就是成员变量。也就是说,一个类中定义的一切变量都属于类的成员变量(甚至是在类中再定义的一个内部类,也属于外部类的的成员变量,只不过这个成员变量是类类型罢了)。
2)成员方法
在Aniaml类
中eat()方法
就是成员方法。同样的,一个类中定义的一切方法都属于类的成员方法。
3)构造方法
这里暂时简单认识一下构造方法,下方类的实例化中会再详细谈一谈构造方法。
在Aniaml类
中Aniaml()
就是构造方法,构造方法是一种特殊的成员方法,目前我们可以观察到的有三点:A.构造方法名必须与类名相同,B.构造方法也是可以传参的,C.构造方法能够被重载。
(2)类的初始化
类的初始化有三种方式:就地初始化、默认初始化、构造方法初始化。其中第三者方式将在下方 再谈构造方法 的时候详谈。
1)就地初始化
class Animal {
public String name = "我是动物!";
public int age = 2;
}
所谓就地初始化即:在类定义之初,当即就为类赋上对应的值。
2)默认初始化
class Animal {
public String name;
public int age;
}
而默认初始化就是:不用赋任何值。因为:实例化类的成员变量会存储在堆上,而堆上的变量会被附上该数据类型的默认值。读者可以阅读看完下方的部分内容之后再回来看,这样会好理解一些。
3.类的实例化
上方我们以及知道如何去定义一个类了,那么如何去使用这个类呢?这就涉及到类的实例化。
类的实例化就是:用自定义的类类型 去定义一个对象。单纯的文字比较难理解,下面我们结合代码来理解,笔者编写了Dog类
辅助说明。
class Dog{
public String name;
public int age;
public void eat() {
int teeth = 4;
System.out.println(teeth + "个牙的狗在吃骨头!");
this.bark();
}
public void bark() {
System.out.println("狗在汪汪叫!");
}
}
public class TalkAboutClass {
public static void main(String[] args) {
Dog dog1 = new Dog();
Dog dog2 = new Dog();
dog1.name = "旺财";
dog2.name = "来福";
dog1.age = 1;
dog2.age = 2;
dog1.eat();
dog1.bark();
}
}
在上面这段代码中,各位读者需要关注的是Dog dog1 = new Dog(); Dog dog2 = new Dog();
这两行。类的实例化就是借助类这个引用类型,去堆上开辟一块空间,这里笔者再给大家做一次分析。
A.Dog
是自定义类,是我们自定义的引用类型。我们可以根据Dog类
定义出无数的引用变量。
B.dog1 和 dog2
是引用变量,也叫做:引用。引用在栈区开辟空间,空间中存储的是一段经过哈希的地址,该地址指向new Dog()后产生的对象。
C.new
关键字代表在堆上开辟一块连续的内存空间,这块空间用来存储Dog类
中的成员变量。
D.Dog()
是一个没有传参的构造方法。但是我们看到,class Dog
中并没有public Dog()
的构造方法,具体原因我们待会再谈。
综上所述,要实例化对象就需要遵循
类名 + 引用变量名 = new 构造方法
的方式。
在此之后,我们如何去使用我们定义的成员变量与成员方法呢?各位读者应该也发现了:如果需要访问类中的成员变量,就应该用引用变量名.成员变量名
的形式去访问;如果需要调用类中的成员方法,就应该用引用变量名.成员方法名
的形式去调用。
理解完实例化对象之后,我们其实对方法的还有三个疑问:
第一个:为什么在实例化对象之后,堆区上没有存储
eat()方法 和 bark()方法
呢?
回答:成员方法是存储在方法区的。
第二个:如果堆区中不存储跟方法有关的信息的话,实例化对象怎么知道自己还有
eat()方法 和 bark()方法
呢?
因为:创建的对象的时候会创建一个方法栈桢,这个方法栈桢里面存的就是方法区里面对应的方法地址,当实例化对象需要调用方法时,就会去自己的方法栈帧中寻找是否存在该方法的地址。
第三个:反过来,类方法中并没有设置被要调用哪个对象,方法又是如何知道为哪个对象调用呢?
因为:成员方法是由所需调用的对象的引用去调用,那么对象的引用就可以在虚拟机栈中找到方法的地址。
相信对上面三个疑问的解答,能够让各位读者对类在内存上的存储有了更深刻的认识。下方,笔者为示例代码画了一张内存分布图,各位读者可以对照着三个问题的回答再次理解。
(1)再谈构造方法
如果各位读者足够细心,就会发现:在类的实例化中列举的代码示例没有构造方法。那么是不是代表构造方法没有存在的必要呢?答案是 否定的,因为想要实例化对象就必须使用到构造方法。但是为什么上面的示例可以不写构造方法呢?事实是:如果我们不写构造方法,编译器就会帮我们写一个无参的构造方法,只不过编译器并不会将之显示出来。
class Dog{
public Dog() {
}
}
现在各位读者应该能理解为何笔者说:构造方法是一种特殊的成员方法。但其实它还有一个特殊之处:构造方法没有返回类型。
有的读者在这里可能就会发问:这是说明构造方法的返回类型是void
类型吗?答案是否定的,构造方法没有返回类型 与 返回值为void
有极大的差别。
返回void
时,其实是想告诉编译器:目前没有返回值,但这是可选的,将来未必没有。但是构造方法则是:绝对不返回任何东西,而且你也没有任何选择,于是通过某种特殊的设计将其变为不带任何返回参型(包括void
)的方法。
接下来,我们补回 类的初始化 留下来的坑。请各位读者直接阅读下方代码。
class Dog {
public String name;
public int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
}
public class TalkAboutClass {
public static void main(String[] args) {
Dog dog1 = new Dog("旺财",1);
Dog dog2 = new Dog("来福",2);
}
}
通过观察,我们发现用构造方法初始化必须要满足四点:
A.类中必须先定义好需要初始化的成员变量;
B.必须在实例化对象时,用构造方法传参;
C.构造方法必须自己写,而且参数个数、类型必须与传过来时对应;
D.构造方法体中必须有赋值的代码。
最后,我们总结一下构造方法的知识点:
A.构造方法名必须与类名相同,且能够传参,也能够被重载;
B.调用完构造方法之后,实例化对象才实际产生。如果我们不写构造方法,编译器就会帮我们写一个无参的构造方法;
C.构造方法没有返回类型;
D.可以通过构造方法初始化。
(2)this关键字
关于this
关键字,我们最先要解决的问题是:为什么需要this
关键字?我们先看看下面这个构造方法。
class Dog() {
public String name;
public int age;
public void Dog(String name, int age) {
// 为了方便区分我们把等号左边的name叫做 name[L]
name = name;
age = age;
}
}
我们知道构造方法传参的,那么:当传的参数名跟定义的成员变量名相同的时候,Java应该如何区分两个相同的name
呢?name[L]
这个变量代表谁?是代表形参?还是代表成员变量?
答案是:
name[L]
代表的是形参。原因是:局部变量优先。
我们通过编译器也可以验证出答案,如果name[L]
代表的是成员变量的话,最后控制台打印出来的dog1.name
应该是旺财
才是正确的,但却打印出null
。至于为什么打印出null
,这是因为编译器把这当成默认初始化。
如果我们不写把旺财
给局部变量,我们应该怎么办呢?这就需要用到this
这个关键字了。
解决方法:this.成员变量 = 局部变量(形参)
class Dog() {
public String name;
public int age;
public void Dog(String name, int age) {
this.name = name;
this.age = age;
}
}
将代码改成以上形式就正确了,但是为什么添加this
关键字就能解决问题呢?
首先要告诉各位读者的是:
this
关键字代表当前对象的引用。
然而,什么是当前对象呢?所谓当前对象就是:在堆区开辟的整块空间。堆区上的整块空间就是一个对象。那么当前对象的引用就是栈区中的引用变量。因此:this.name = name;
跟dog1.name = "旺财";
本质上没什么区别。
既然,this
关键字代表当前对象的引用,那么用this
来调用成员方法也没什么奇怪的。
class Dog {
public String name;
public int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
this.bark();
}
public void bark() {
System.out.println("狗在汪汪叫!");
}
}
public class TalkAboutClass {
public static void main(String[] args) {
Dog dog1 = new Dog("旺财",1);
System.out.println(dog1.name);
System.out.println(dog1.age);
}
}
除此之外,this
关键字还有特殊的一点:this
可以调用构造方法。不过通过 this() 调用构造方法需要满足以下三个条件:
A.this()
必须在构造方法内部使用
B.必须写在第一行;
C.不要形成闭环(两个构造方法一直相互调用)。
class Dog {
public String name;
public int age;
public Dog(String name) {
this("旺财",1);
this.name = name;
System.out.println("在只有一个参数的构造方法中:" + this.name + "是" + this.age + "岁。");
}
public Dog(String name, int age) {
// this("来福"); -- error 形成闭环调用
this.name = name;
this.age = age;
System.out.println("在有两个参数的构造方法中:" + this.name + "是" + this.age + "岁。");
}
}
public class TalkAboutClass {
public static void main(String[] args) {
Dog dog1 = new Dog("来福");
}
}
这里比较有趣的一点是旺财也是1岁
。原因就是:Dog(String name)
这个构造方法中,使用this()
调用构造方法,让age
在Dog(String name, int age)
这个构造方法中被构造方法初始化了。
(3)static关键字
static
是静态关键字,一旦被static修饰,被修饰者将会被存储到方法区中,其生命周期将延长至与整个类相同,而static
修饰的变量和方法,在类加载的时候就完成初始化。笔者将分别对 访问成员变量 、 访问成员方法 进行介绍。
A.访问成员变量
在bark1()
这个非静态方法中,我们可以看到:无论对象是否被static
修饰,都可以轻易访问到。只不过在访问 静态成员变量 的时候,我们需要注意代码的编写方式,最好使用 TestForBlog.teeth
的方式,即:类名.静态成员变量
在fly1()
这个静态方法中,我们却看到编译器给我们报了两个错误。
对于error1
,我们可以得知:应该使用类的引用去访问一个 非静态成员变量 ,否则直接访问会报错。
对于error2
,我们可以得知:静态成员变量不能用引用去访问,因为其不属于类的对象,而属于整个类,所以需要用类名去访问。
B.访问成员方法,笔者同样分成同一个类中和不同类中的情况,请各位读者自行阅读下方代码:
在bark1()
这个非静态方法中,我们可以看到:无论对象是否被static
修饰,同样可以被访问到。
在fly1()
这个静态方法中,我们却看到编译器给我们报了个错误。
这就说明:我们要在静态方法中访问非静态方法只能先实例化对象。
浅浅总结~
1.非静态方法啥都能访问,访问静态 或 非静态 的 成员变量 与 成员方法 都没毛病!
2.静态方法在访问非静态成员变量 或 非静态成员方法的时候,都需要实例化对象,用对象去访问
3.真正好地访问静态变量、调用静态方法的方式是:类名.成员变量名 或 类名.成员方法名
在了解了上面的知识后,有的读者又联想起刚刚提到的构造方法,想在构造方法中搞搞事情,想要修饰构造方法中的局部变量。
显而易见,这是不可行的。被static
修饰的局部变量是属于类本身而不属于类的对象,而构造方法的出现就意味着类的对象的实例化,两者根本是相矛盾的,因此错有所究。
static
还有一个重要的知识点:静态方法无法重写,无法也用来实现多态。这个的内容等到介绍多态的时候再给大家详细解释。
最强总结
static
三大要点:
1.static
修饰的成员方法(静态方法)无法访问 非静态成员变量 或 非静态成员方法;
2.正确访问静态变量、调用静态方法的方式是:类名.成员变量名 或 类名.成员方法名
;
3.static
不能修饰构造方法中的局部变量。
最有灵魂的一句话:static
是为类服务的。
4.代码块
代码块指:用{ }
包起来的这部分代码,代码块一共可以分成四类:普通代码块、实例代码块、静态代码块、同步代码块。
(1)普通代码块
普通代码块:定义在方法中的代码块。
public class TestForBlog {
public static void main(String[] args) {
{
System.out.println("这是普通代码块!");
}
System.out.println("这是普通代码块的外部!");
}
}
(2)实例代码块
实例代码块:定义在类中的代码块(不加修饰符)。实例代码块只有在创建对象时才会执行。
class Dog{
public String name;
public int age;
public static int teeth;
{
name = "旺财";
}
}
(3)静态代码块
静态代码块:使用static
定义的代码块。一般用于初始化静态成员变量。值得注意的是:静态代码块不管生成多少个对象,其只会执行一次。
class Dog{
public String name;
public int age;
public static int teeth;
static {
teeth = 4;
}
}
光谈上面的内容不足以展现出代码块的能耐,请读者自行阅读下方代码,并猜测代码的运行结果。
class People {
public String name;
public int age;
public static int id;
static {
id = 1;
System.out.println("这里是静态代码块!");
}
{
System.out.println("这里是实例化代码块!");
}
public People(String name, int age) {
this.name = name;
this.age = age;
System.out.println("这里是构造方法!");
}
}
public class TestForBlog {
public static void main(String[] args) {
People people1 = new People("大白",1);
System.out.println("===========================");
People people2 = new People("小黑",2);
}
}
程序的执行顺序是非常重要的问题,因此我们需要特别关注这一现象。在查看上方代码执行结果时,我们会很神奇的发现:代码块能在构造方法前执行。然而为什么静态代码块又比实例化代码块优先执行呢?为什么静态代码块只能执行一次呢?
因为静态代码块在类加载阶段就完成了,而一个类只能被加载一次。
5.内部类
内部类是定义在类或者方法里面的类。一共可以分成四种内部类:实例内部类(构造内部类)、静态内部类、局部内部类、匿名内部类。
class OuterClass {
class InnerClass {
}
}
那么,我们应该如何在主方法中去访问内部类呢?
OuterClass outerClass = new outerClass():
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
OuterClass.InnerClass
不难理解,只需要把内部类当成外部类的成员就行,而成员就是通过.
去访问的。
比较难理解的是outerClass.new InnerClass()
。很多读者会被这个空格所误导,以为是分成outerClass.new
与InnerClass()
两部分去理解。正好相反,我们是:在外部类的堆区空间中,开辟内部类的空间。因此应该是通过外部类的引用outerClass
找到内部空间后,在开辟一块空间new InnerClass()
。
如果我们理解了标黄的句子,那么上方两行的实例化的代码可以被改成下面这一行:
OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();
(1)实例内部类
A.内部类成员
实例内部类与静态内部类相对,因此我们可以说,实力内部类是:未被static
修饰的成员内部类。
class OuterClass {
public int data1 = 1;
public static int data2 = 2;
// 实例内部类
class InnerClass {
public int data4 = 4;
public static final int data5 = 5;
}
}
上方这个代码示例中,最值得我们关注的就是public static final int data5 = 5;
部分读者会疑惑:内部类作为外部类的成员,也属于 类的对象,为什么被 static final
修饰就可以成功通过编译?
部分读者还会联想到上方提到的:static
不能修饰构造方法中的局部变量。那么是不是意味着 构造方法中的局部变量被 static final
修饰就能够定义了呢?
首先要回答第二个问题,这是错误的!只要构造方法中的出现static
修饰,都是错误的。因为static
为类服务,只要被它修饰就属于类,哪怕被final
修饰后变成常量,也属于类,不属于方法。
至于第一个问题,各位读者应先知道 final
修饰局部变量时,这个局部变量会变成常量,而常量在编译阶段就确认了。(这里插一句无关的但又重要的话:一个成员存储在哪里跟final
修饰没有关系)我们只能说,内部类非常特殊,作为外部类的成员,自己就是一个类,所以才有static final
这种骚操作。
B.外部类与内部类的访问
最关键的只有一句话:实例内部类可以访问任何外部类,外部类在访问实例内部类时,需要先实例化内部类。
这里会遇到一个对于初学者比较棘手的问题:外部类与内部类的成员同名怎么办? 很普遍的解决办法:就近原则,使用内部类的成员。
class OuterClass {
public int data1 = 1;
public int data2 = 2;
class InnerClass {
public int data1 = 11;
public int data3 = 3;
public void test() {
System.out.println(data1);
System.out.println(data2);
System.out.println(data3);
}
}
}
public class TestForBlog {
public static void main(String[] args) {
OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();
innerClass.test();
}
}
这时候一定会有部分读者想问,我就是想访问外部类的同名成员需要怎么办?
笔者也非常想知道这个问题,所以我探究了一下,发现有两种方式。
// 法一:
class OuterClass {
public int data1 = 1;
public int data2 = 2;
class InnerClass {
public int data1 = 11;
public int data3 = 3;
public void test() {
OuterClass outerClass = new OuterClass();
System.out.println("外部类data1: " + outerClass.data1);
System.out.println("内部类data1: " + data1);
System.out.println(data2);
System.out.println(data3);
}
}
}
直接在内部类中初始化一个外部类,这样肯定能够访问到外部类中的成员。但这种方法未免显得笨拙不少,于是就有了下面这个方法。
// 法二:
class OuterClass {
public int data1 = 1;
public int data2 = 2;
class InnerClass {
public int data1 = 11;
public int data3 = 3;
public void test() {
System.out.println("外部类data1: " + OuterClass.this.data1);
System.out.println("内部类data1: " + data1);
System.out.println(data2);
System.out.println(data3);
}
}
}
上方这段代码最主要是对OuterClass.this.data1
怎么理解。其实就是分成OuterClass.this
与data1
两个部分。OuterClass.this
得到的就是外部类的this
,通过外部类的this
访问data1
,得到的当然是外部类的同名成员变量。
关于实例外部类的总结
1.外部类中的任何成员都可以在实例内部类方法中直接访问(哪怕被private
修饰);
2.外部类不能直接访问实例内部类中的成员,如果要访问必须先要创建内部类的对象;
3.实例内部类所处的位置与外部类成员位置相同,因此也受public、private
等访问限定符的约束;
4.在实例内部类方法中访问同名的成员时,优先访问自己,如果要访问外部类同名的成员,用:外部类名称.this.同名成员
来访问;
5. 实例内部类的非静态方法中包含了一个:指向外部类对象的引用外部类名称.this
(2)静态内部类
首先我们最先提出的问题是:如何获取静态内部类 ? 其实很简单,因为访问static
修饰者通通遵循:类名.成员
的方式。于是就有了下面这行代码:
OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
属于静态内部类的特性就是:不能访问外部类的非静态成员。外部类的非静态成员,需要通过外部类对象的引用才能访问。
class OuterClass {
public int data1 = 1;
public static int data2 = 2;
public void test() {
System.out.println("外部类非静态方法");
}
static class InnerClass {
public int data3 = 3;
public static int data4 = 4;
public void func() {
//System.out.println(data1); // error
System.out.println(data2);
System.out.println(data3);
System.out.println(data4);
OuterClass outerClass = new OuterClass();
System.out.println("外部类data1:" + outerClass.data1);
outerClass.test();
}
}
}
public class TestForBlog {
public static void main(String[] args) {
OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
innerClass.func();
}
}
(3)局部内部类
局部内部类是指定义在外部类的方法体或者{}
中的类。这也意味着:A.只能在方法中调用类; B.不能被public 或 static
修饰。
(4)匿名内部类
首当其冲的应该是:何为匿名对象。匿名对象指只能使用一次的对象。因此每次使用的时候都要重新new
.
匿名内部类需要涉及到接口的知识,所以这里暂且不谈。
public class InnerClass
{
public static void main(String[] args) {
new Person();
// 每次使用的时候都要重新 new
System.out.println(new Person().age);
System.out.println(new Person().name);
// 匿名内部类
new Person() {
};
}
}
结语
类和对象的知识非常繁杂,且非常抽象,我们很难用一两天就理解清楚,在学习的时候要慢慢来,多敲代码感受感受,要自己抽时间回顾知识点。总之,希望各位加油,也希望这篇万字长文对你有所帮助!