1. 类和对象的概念
首先,Java是一门面向对象的编程语言.
在学习过程中,我们或多或少都会听到"面向对象"或"面向过程"这两个名称,但是如何理解它们呢?
要了解它们之间的区别,首先要理解类和对象的概念.
1.1 现实世界中的类和对象
- 类就是具有相同特点事物的集合,比如:人类,狗的品种,颜色的种类…
- 对象是类的一个"实例",实例具有状态和行为."实例"就是从类里面拿一个具体的元素出来.比如从人类集合中拿一个人出来,他的状态可能是开心的,行为可能是在吃饭,等等.
(图片来源于网络)
1.2 Java中的类和对象
实际上,Java中的类和对象的概念与现实中并无二致,只是集合的元素发生的改变.Java中的元素可以是某个字符串,一个数组等等.
Java中的对象(软件对象)也是有状态和行为的,这非常重要.
- 软件对象的状态就是它们的属性.
- 软件对象的行为通过方法(函数)实现.
1.3 理解面向对象
了解了类和对象的概念后,我们可以较容易地理解面向过程和面向对象的区别
- 面向过程:关注过程,把要完成某件事的所有过程都分层次做好,然后根据需要将各个子功能块组合起来,从而实现某些具体的功能.即结构化编程
- 面向对象:关注对象.不论完成某件事的步骤有多复杂,都将对数据本身和对数据的操作放在一起,将它们视为整体,这个整体就是对象.面向对象就是跟要处理的数据的所有事情都视为一个整体.
1.3.1 面向对象程序设计
面向对象程序设计主要有三步
- 找对象
- 创建对象
- 使用对象
其中创建对象我们在之前的学习实际上就已经接触过了,比如new一个新的字符串.
2. 类和类的实例化
上面提到,类是一类对象的集合,对象是集合的一个实例.
这里需要注意Java中类的特点:类是创建对象的模板
对象是类的样本,一个类可以实例化若干个对象.
需要说明的是,"模板"是不初始化变量的,因为不同对象的变量的值可能不同.
2.1 创建类
创建一个类也叫声明一个类,Java中创建一个类就是创建一个新的类型,可以用C的结构体来理解.
类在Java中属于引用变量,使用class
声明类.
首先需要注意的是,上文提到:对象的状态通过属性体现,也就是变量.一个类可以以下几种变量:
- 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
- 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
- 类变量:类变量也声明在类中,方法体之外,但必须声明为 static 类型。
对象的行为通过方法实现,所以一个类的创建至少需要一个方法,也可以有多个方法.后文会详细介绍.
2.1.1 语法
声明类:
class ClassName{ field;//成员属性 method;//成员方法 }
注: class
为定义类的关键字,ClassName
为类的名字(大驼峰),{}
中为类的主体.
主体中的所有变量称为成员属性或字段,函数称为成员方法.
以定义一个Person
类为例:
人有姓名,性别和年龄等状态,有吃饭睡觉等行为.
//声明一个Person类 class Person { //成员变量 public int age; public String name; public String sex; //成员方法 public void eat() { System.out.println("吃饭"); } public void sleep() { System.out.println("睡觉"); } }
注:在初学时写的方法一般带有static
关键字,此处是不带的,后面会介绍其作用.
2.2 类的实例化
我们把用类创建对象的过程称为实例化,所以类的实例化就是创建对象的过程.
用一个例子理解:
类是房子的设计蓝图,类的实例化就是根据蓝图建房子的过程.而房子的装修是后来才添砖加瓦的.根据蓝图建的房子只是一个样板房.
要点:
- 类只是一个模板.属性和方法都需要另外初始化.
- 创建的对象是储存着成员变量的,所以它是由占用内存空间的.
2.2.1 语法
ClassName 对象名 = new class_name();
以上面的Person
类为模板,创建对象为例:
Person xiaoming = new Person();
2.3 使用对象的成员方法
//声明一个Person类 class Person { //成员变量 public int age; public String name; //成员方法 public void eat() { System.out.println("吃饭"); } public void sleep() { System.out.println("睡觉"); } } public class TestDemo { public static void main(String[] args) { Person xiaoming = new Person(); xiaoming.eat(); xiaoming.sleep(); } }
结果:
吃饭
睡觉
关于使用成员变量的用法在后面会介绍.
ps:
- 通过
.
操作符访问对象中的字段.- "访问"包括读和写.
- 未初始化的字段的值是默认值.
3. 类的成员
类的成员包含以下:字段、方法、代码块、内部类和接口等
在此只介绍字段、方法和代码块.
3.1 字段
字段也叫成员变量或属性
字段表示与对象或类关联的变量.即描述一个类中包含有哪些数据.
public class TestDemo { public static void main(String[] args) { Person xiaoming = new Person(); System.out.println(xiaoming.age); System.out.println(xiaoming.name); System.out.println(xiaoming.sex); } }
结果:
0
null
null
默认值:
- 数值类型:0
- 引用类型(String ,自定义类型):null
- Boolean:false
3.1.1 字段就地初始化
字段就地初始化即在类中初始化字段.
//声明一个Person类 class Person { //成员变量 public int age = 18; public String name = "xiaoming"; public String sex; } public class TestDemo { public static void main(String[] args) { Person xiaoming = new Person(); System.out.println(xiaoming.age); System.out.println(xiaoming.name); } }
结果:
18
xiaoming
这样做的弊端:
类是模板,如果修改了模板中的内容,那么另外实例化的对象的成员也会被修改,这是不符合常理的.所以如果属性不是通用的,要在对象上初始化字段.
3.2 成员方法
前文提到,对象的行为通过成员方法实现.
class Person { //成员变量 public int age = 18; public String name = "xiaoming"; public void speek() { System.out.println("我叫"+name+",我"+age+"岁了"); } } public class TestDemo { public static void main(String[] args) { Person xiaoming = new Person(); xiaoming.speek(); } }
结果:
我叫xiaoming,我18岁了
用speek
表示xiaoming
这个对象具有speek
的行为.
3.3 static 关键字
static关键字可以修饰属性,方法,代码块和类.其中类在内部类
中会介绍.
3.3.1 修饰属性
属性也叫成员变量.
成员变量分为普通成员变量和静态成员变量,前者属于对象,后者不属于对象.
例:
class Test { public int a = 10; public static int count = 0; } public class TestDemo { public static void main(String[] args) { Test test1 = new Test(); test1.a++; Test.count++; System.out.println(test1.a); System.out.println(Test.count++); System.out.println("-------------"); Test test2 = new Test(); test2.a++; Test.count++; System.out.println(test1.a); System.out.println(Test.count++); } }
上面在类中创建了一个普通成员变量a
和静态成员变量static
,然后先后new
了两个对象test1
和test2
,同时对两个变量++
,打印其值.
结果:
11
1
11
3
可以发现被static
修饰的静态成员变量的值没有随着新建对象而重新初始化为10.
上文提到,静态成员变量不属于对象,那它属于谁呢?
我们通过内存图示理解:
所以我们可以知道:静态成员变量存放在方法区中,这块内存是所有线程共享的.而且静态成员变量和类是在同一块内存的.正因为静态成员变量不属于对象而属于某个类,所以上面不是通过引用.count++
而是通过类名.count++
访问静态成员变量.
所以无需专门new
对象来访问静态成员变量.
静态成员变量也可以认为是类的变量.这是合理的,因为访问静态成员变量也是通过类名访问的.
3.3.2 修饰方法
被static
修饰的方法称为静态方法.
否则是普通方法.
例:调用类中的静态方法
class Test1 { //这是一个静态成员变量 public static int a = 1; //这是一个静态方法 public static void func() { System.out.println("这是一个静态方法"); } } public class TestDemo { public static void main(String[] args) { Test1.func(); System.out.println(Test1.a); } }
结果:
这是一个静态方法
a=1
注意事项:
- 静态方法同样属于类而不属于对象,所以同静态成员变量一样,要通过
类名.方法名
调用静态方法. - 静态方法只能访问类中的静态成员变量,且可以修改其值.
- 普通方法可以调用静态方法,反之则否.它们的"地位"不同,因而依赖性,所以权限不同.当然,可以通过
new
一个对象来实现静态方法调用普通方法,不过一般情况下不会这么做. - 普通方法和静态方法中都不能定义静态变量.
原因:
- 静态变量属于类.
- 普通方法在类外部是通过引用访问,静态变量通过类名访问.
- 不论是通过引用访问静态变量还是类名访问静态变量,都会发生冲突.
附:
static修饰与否,取决于实际情况.实际上,main方法是否被static修饰也取决于JVM.
static是决定对象存在方法区还是堆区的关键字,与
final
关键字无关.问:引用一定是在栈上吗?
4. 封装
4.1 概念
在面向对象编程方法中,封装(英语:Encapsulation)是指,一种将抽象性函数接口的实现细节部分包装、隐藏起来的方法。同时,它也是一种防止外界调用端,去访问对象内部实现细节的手段,这个手段是由编程语言本身来提供的。封装被视为是面向对象的四项原则之一。
适当的封装,可以将对象使用接口的程序实现部分隐藏起来,不让用户看到,同时确保用户无法任意更改对象内部的重要资料,若想接触资料只能通过**公开接入方法(Publicly accessible methods)**的方式( 如:“getters” 和"setters")。它可以让代码更容易理解与维护,也加强了代码的安全性。
–来自维基百科
在面向对象编程的过程中,有两个角色:类的实现者,类的使用者.封装就是为了更好地贴合"面向对象"的思想,让类的使用者能更专注的面对对象,无需太了解类是如何实现的,只需知道如何使用类即可.这降低了类使用者的学习和使用成本,提高了效率.
4.2 实现封装
背景
假如类的实现者用public
修饰类中所有的成员变量和方法,并且类的使用者访问/调用了它们.
class Person { int age = 18; String name = "小明"; } public class TestDemo { public static void main(String[] args) { Person person = new Person(); System.out.println("我叫"+person.name+",今年"+person.age+"岁"); } }
结果:
我叫小明,今年18岁
这个程序在外部访问了类中的成员,并打印了它们的值.
(假如)如果实现类的人手贱,将类中的成员的值修改了,那么所有使用类的成员的值的地方都会被修改,这是非常致命的.
所以在类中不加以限制访问地创建成员会带来弊端:
- 给类的使用者增加负担:类的使用者必须了解类的内部是如何实现的,增加了使用成本.
- 一旦类的实现者修改了代码,例如修改了成员的值,修改了成员的变量名等,都让代码的维护变得异常困难.
有什么办法可以解决这个问题呢?
修改访问成员的权限.
4.2.1 private 关键字
private
和public
是两个控制访问权限的关键字.
- 被public修饰的成员变量或方法可以被类的调用者访问/调用.
- 被public修饰的成员变量或方法不可以被类的调用者访问/调用.
private
不让用,而public
让用,我们是不是可以组合一下它们,用private
修饰成员,用public
修饰方法,让这些方法作为桥梁,类的使用者间接地通过这些方法访问或调用成员.这便是封装.
class Person { private int age = 18; private String name = "小明"; public void print() { System.out.println("我叫"+name+",今年"+age+"岁"); } } public class TestDemo { public static void main(String[] args) { Person person = new Person(); person.print(); } }
结果:
我叫小明,今年18岁
这么做,正好与上文所述的封装的概念对应.类的使用者只需知道类是如何使用的即可,无需了解其内部实现.
也解决了使用public
修饰全部成员带来的弊端.即使类的实现者修改了代码,也不会影响外部的使用.
小结:
- 如何封装?
- 用
private
修饰字段.- 用
public
修饰某些需要外部用到的方法.
- 封装的好处?
- 让属性相对更安全,使"面向对象"的过程更存粹.
- 被封装的成员只能在当前类中被使用.
通过上面的例子我们知道,被封装的成员是无法在类的外部被访问和使用的,那如何访问和使用它们呢?
使用接口进入类的内部,在类的内部访问和使用.
4.3 getter与setter方法(类)
在Java中,getter和setter是两种常规方法,用于检索和更新变量的值。 因此, setter 是一种更新变量值的方法。 getter 是一种读取变量值的方法。 getter 和 setter 在Java 中也称为访问器和更改器。
class Person { private int age; private String name; //设置名字 public void setName(String name) { this.name = name; } //得到名字 public String getName() { return name; } } public class TestDemo { public static void main(String[] args) { Person person = new Person(); //调用set方法,传入"小明" person.setName("小明"); //调用get方法并打印 System.out.println(person.getName()); } }
结果:
小明
注意事项
- 在set方法中,出现了新的关键字
this
,它用来当前对象的使用.使用的前提条件是当这个set的形参名称和类中的成员变量的名称一样时,因为这样的情况下,编译器并不知道这个变量是类中的还是set方法中的. - setter和setter根据需要添加.
5. 构造方法(对象)
5.1 介绍
构造方法,是一种特殊的方法,它是一个与类同名的方法。 对象的创建就是通过构造方法来完成,其功能主要是完成对象的初始化。 当类实例化一个对象时会自动调用构造方法。 构造方法和其他方法一样也可以重载。
顾名思义.构造方法就是用来构造对象的方法.
5.2 默认构造方法
class Person { private int age; private String name; //打印方法 public void print() { System.out.println("age:" + age+" name:"+name); } } public class TestDemo { public static void main(String[] args) { //new一个对象 Person person = new Person(); //打印 person.print(); } }
结果:
age:0 name:null
此时,类中并无任何构造方法,所以编译器会自动添加一个无参数无语句的构造方法.
public Person() { }
这个默认无参数的构造方法决定了new对象的时候是否传参.
5.3 有参数的构造方法
假若写的构造方法有参数,new对象的时候必须传对应数量的参数
class Person { private int age; private String name; //设置名字 public Person(int age) { this.age = age; } public Person(int age, String name) { this.age = age; this.name = name; } public void print() { System.out.println("age:" + age+" name:"+name); } } public class TestDemo { public static void main(String[] args) { Person person = new Person(18); person.print(); Person person1 = new Person(20,"小明"); person1.print(); } }
结果:
aage:18 name:null
age:20 name:小明
5.4 总结
- 构造方法的名称和类名相同,需要用
public
修饰. - 实例化对象的时候,如果没有构造函数,则不能传参.
- 当类中有多个构造函数时,实例化时参数的数量可以对应任意一个构造函数的数量.但若类中无自定义构造函数时,默认构造函数无参数.
- 构造方法之间构成重载.
5.5 this(补充)
上文提到"this
表示当前对象"是错误的.实际上this
有三种用法.在这里仅为了说明前面那句话是错误的.
class Person { private int age; private String name; public Person() { //this调用构造函数 this(18,"小明"); } public Person(int age, String name) { this.age = age; this.name = name; } public void print() { System.out.println("age:" + age+" name:"+name); } } public class TestDemo { public static void main(String[] args) { //调用不带参数的构造函数 Person person = new Person(); person.print(); } }
结果:
age:18 name:小明
还未调用完构造函数,就是对象还没被构造完成就使用了this
关键字,它不代表当前对象,而代表当前对象的引用.
6. 代码块
字段的初始化方式:
- 就地初始化
- 使用构造方法初始化
- 使用代码块初始化
下面介绍第三种方法.
6.1 概念
简单地说,代码块就是{}
括起来的一段代码.
- 普通代码块
- 构造块
- 静态块
- 同步代码块(学习多线程时会遇到)
6.2 普通代码块
定义在方法中的代码块,即方法的{}
括起来的代码.
public class TestDemo { public static void main(String[] args) { System.out.println("hello world"); } }
其中System.out.println("hello world");
就是普通代码块.
6.3 构造代码块
构造代码块:定义在类中的代码块(不加任何修饰符,只由{}
括起来的代码),也叫做实例代码块。构造代码块一般用于初始化实例成员变量。
class Person { private int age; private String name; //这是一个构造方法 Person() { System.out.println("构造方法"); } //这是一个实例代码块 { this.name = "小明"; this.age = 18; System.out.println("实例代码块"); } //打印方法 public void print() { System.out.println("age:" + age+" name:"+name); } } public class TestDemo { public static void main(String[] args) { //new一个对象 Person person = new Person(); //打印 person.print(); } }
结果:
实例代码块
构造方法
age:18 name:小明
- 实例代码块是最先被执行的.
- 它的作用就是初始化实例.
6.4 静态代码块
被static
修饰的,且被{}
括起来的代码块.
用于初始化静态成员属性.
class Person { private int age; private String name; private static int s; //这是一个构造方法 Person() { System.out.println("构造方法"); } //这是一个实例代码块 { System.out.println("实例代码块"); } //这是一个静态代码块 static { s++; System.out.println("静态代码块"); } //打印方法 public void print() { System.out.println("静态成员变量s="+s); } } public class TestDemo { public static void main(String[] args) { //new一个对象 Person person = new Person(); //打印 person.print(); System.out.println("---------"); //再new一个对象 Person person1 = new Person(); person1.print(); } }
结果:
静态代码块
实例代码块
构造方法
静态成员变量s=1
实例代码块
构造方法
静态成员变量s=1
以上代码做的事情:
- 在类中写了实例代码块,静态代码块和构造方法(注意这里故意将静态代码块放在最后).每个代码块和构造方法都打印各自的名字,以观察它们被调用的先后顺序.
- 在main方法中先后new了两个对象,都进行打印.
结果表明:
- 不论静态代码块,实例代码块和构造方法的先后顺序如何,它们的被调用顺序是静态代码块->实例代码块->构造方法.
- 不论有多少个对象,静态代码块有且只被调用一次.实际上,不论有没有对象,静态代码块都会被调用.
注意事项:
- 静态代码块中只能是静态成员变量.
- 静态代码块有且只被调用一次.
- 静态成员变量的初始化与否,和静态成员变量与静态代码块的前后位置会影响最终静态变量的结果.
7. 补充
7.1 toString方法
实际上toString方法在学习数组时已经使用过了.当我们想打印某个对象内容时,如果直接将引用作为参数,打印出来的结果是一个由地址哈希加密的伪地址.(因为Java不希望用户拿到变量的地址,以保证足够安全)
toString方法的作用是将对象转换为字符串,其返回值是字符串类型.
首先要知道这个伪地址是如何被生成的(是哪个方法生成的,不考虑原理)
class Person { private String name; private int age; public Person(String name,int age) { this.age = age; this.name = name; } public void show() { System.out.println("name:"+name+" " + "age:"+age); } } public class TestDemo { public static void main(String[] args) { Person person = new Person("小明",18); person.show(); System.out.println(person); } }
当然,结果是一个伪地址.
name:小明 age:18
Person@1540e19d
若编译器是IEDA,这里按住win/command,单击println
,会跳转到这个函数:
public void println(Object x) { String s = String.valueOf(x); synchronized (this) { print(s); newLine(); } }
继续操作,点击Object
,在跳转的页面往下翻,会看到:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
可以看到,println
方法本身就是调用toString
方法的.
其格式是:类名 + "@" + 类对象的哈希code码
但默认的toString
方法不能很好地满足我们的需要,所以我们可以自己实现一个toString
方法,让println
方法调用我们自己实现的toString
方法.实际上这与默认的toString
方法构成重写.
class Person { private String name; private int age; public Person(String name, int age) { this.age = age; this.name = name; } public void print() { System.out.println("name:" + name + " " + "age:" + age); } //重写Object的toString方法 @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class TestDemo { public static void main(String[] args) { Person person = new Person("小明", 18); person.print(); System.out.println(person); } }
结果:
name:小明 age:18
Person{name=‘小明’, age=18}
重写toString
方法的规则:
- 必须被
public
修饰. - 返回类型为
String
. - 方法的名称必须为
toString
,且无参数
. - 方法体中不要使用输出方法
System.out.println()
.
注意事项:
println
会自动调用toString
方法.- 将对象转化为字符串的行为叫做序列化.序列化需要和反序列化一起理解:简单地说,序列化就是加密的过程,反序列化就是解密的过程,它们遵循的规则是相同的.
toString
是Object
类提供的方法.自定义的类(比如Person
类)默认继承自Object
类.所以可以在Object
的子类重写toString
方法.(这是继承和重写的知识)@Override
在 Java 中称为 “注解”, 此处的@Override
表示下面实现的toString
方法是重写了父类的方法.
关于Object类:
Object 是 Java 类库中的一个特殊类,也是所有类的父类。也就是说,Java 允许把任何类型的对象赋给 Object 类型的变量。当一个类被定义后,如果没有指定继承的父类,那么默认父类就是 Object 类。
7.2 匿名对象
匿名对象是没有名字的对象(说了跟没说一样).
Talk is cheap. Show me the code.
class Person { private String name; private int age; public Person(String name, int age) { this.age = age; this.name = name; } public void print() { System.out.println("name:" + name + " " + "age:" + age); } } public class TestDemo { public static void main(String[] args) { //这是一个匿名对象 new Person("小明", 18).print(); } }
结果:
name:小明 age:18
结果非常的amazing,为什么没有名字的对象还能调用类的方法呢?
首先了解匿名对象的注意事项:
- 没有引用的对象叫匿名对象.
- 匿名对象只能在创建对象时使用.
- 匿名对象是一次性的.也就是说,匿名对象只能在对象被创建时才能发挥作用.
- 匿名对象调用类方法时参数要对应.
我们知道,我们是通过对象的引用来调用类中的方法的,但匿名对象没有引用,那么我们就把它看成一个整体,这也是只能它只能在创建对象时使用和具有一次性的原因.
总结
一些体会
千磨万击还坚劲.“面向对象"是Java中最基础的部分,如果理解的话,难度不是很大(又是一句废话).后面深入理解了继承,多态等知识点并加以应用,才算是搞懂了.但并不能称之为精通,只能算是入门.就像学习C的指针部分,很多人传言这个很难,但在自己专心学完后,不禁感叹"就这?”(当然老师起着至关重要的作用,非常感谢鹏哥!)
在学习这块知识时,抛开知识本身,我认为一颗宁静的心对学习某一块密集的知识是非常重要的,因为只要你稍不注意,某块地方就会卡壳,造成一系列消极影响.所以不管做什么事,只要是你认为它在将来有可能对你有用,那就做下去吧.
知识总结
- 理解类和对象的概念
- 类是对象的模板
- 对象是类的实例化
- 模板和实例化的区别就像毛坯房和精装房之间的区别
- 类的成员根据是否被
static
修饰分为两种:普通成员变量和静态成员变量.普通成员变量属于对象,静态成员变量又叫做类变量,属于类.二者在内存中的存储位置也不同.因而它们的权限不同,故使用方法有所区别. - 封装就是让即使在类中被修改也不能影响外部使用的东西只限制在一个类中,而getter和setter方法就是使用了public和private两个关键字的特性,灵活地让用户通过它们访问或修改对象中被封装的变量.这对程序员很友好.
- 构造方法就是为毛坯房装修的工具.注意this的使用.
- 静态代码块->实例代码块->构造函数
注意:封装的对象是
类中的成员
,构造方法的对象是对象
.