一、面向对象程序设计概述
1.面向对象与面向过程
面向过程: 面向过程,你自己为执行者,依次执行所有你要做的事情。
面向对象:面向对象,你自己为指挥者,指挥某些具有相应功能的对象去完成你要做的事情,你本人不需要知道功能实现的过程,只需要调用对象所具备的方法。
2.对象与类
对象:现实中存在的个体,java中对应为在堆内存中产生一个实体 。
类:是对现实事物的描述,是提取对象的共性内容,是对具体的抽象。
由类构造对象的过程称为创建类的实例(对象)。对象中的数据称为实例字段(成员变量),操作数据的过程称为方法(成员函数)。这些值的集合就是这个对象的当前状态。我们可以通过调用方法实现对象状态的改变。
3.引用:我们把指向一个对象的变量称为类类型变量,又叫做引用。它是一个局部变量,作用于函数或语句中,随着函数或语句的结束而消失,被存放于栈内存中。
eg. Car c = new Car();
当我们执行上述语句时,在栈内存和堆内存中分别产生一个局部变量和一个实体,引用c指向这个对象。可以有多个引用都指向同一个对象:Car c1 = c;
4.匿名对象:匿名对象未给对象取名,调用匿名对象属性没有任何意义,只有方法调用有意义。
new Car().num = 5; //无意义
new Car().run(); //有意义
匿名对象使用情形:
1.对对象方法只调用一次时。
2.作为实参进行传送,eg.show(new Car())
;
而show方法中有形参,public void show(Car c)
,实际上为Car c = new car
,即c指向这个对象,就可以通过show方法来改变这个对象,这时也是有意义的。
5.封装:封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式,仅对外提供公共的访问方式。
实现封装的关键在于,绝对不能让类中的方法直接访问其他类的字段。程序只能通过对象的方法与对象数据进行交互,封装给对象赋予了“黑盒”特征,这是提高重用性和可靠性的关键。
6.如何实现封装:
有些时候我们直接操作某个封装体的字段或方法,容易造成安全隐患或者逻辑混乱。为了避免外部代码直接去访问字段或方法,我们可以用private修饰,拒绝外部访问:
class Person {
private String name;
private int age;
}
7.更改器与访问器方法:
访问器方法:只访问对象而不修改对象的方法。
更改器方法:可以更改调用这个方法的对象的方法。
public int getAge() { //访问器方法
return this.age;
}
public void setAge(int age) { //更改器方法
if (age < 0 || age > 100) {
throw new IllegalArgumentException("invalid age value");
}
this.age = age;
}
虽然外部代码不能直接修改private字段,但是,外部代码可以调用方法setName()和setAge()来间接修改private字段。并且哪个对象调用这些方法,就修改哪个对象堆内存里的值。
在方法内部,我们就有机会检查参数对不对。比如,setAge()就会检查传入的参数,参数超出了范围,直接报错。这样,外部代码就没有任何机会把age设置成不合理的值。
8. private方法:定义private方法的理由是内部方法可以调用private方法。
class Person {
private String name;
private int birth;
public void setBirth(int birth) {
this.birth = birth;
}
public int getAge() {
return calcAge(2019); // 调用private方法
}
// private方法:
private int calcAge(int currentYear) {
return currentYear - this.birth;
}
calcAge()是一个private方法,外部代码无法调用,但是内部方法getAge()可以调用它。
Person类只定义了birth字段,没有定义age字段,获取age时,通过方法getAge()返回的是一个实时计算的值,并非存储在某个字段的值。这说明方法可以封装一个类的对外接口,调用方不需要知道也不关心Person实例在内部到底有没有age字段。
9.this:关键字this指示隐式参数,它始终指向当前实例。如果没有命名冲突,可以省略this。但是,如果有局部变量和字段重名,参数变量会遮蔽同名的实例字段,但还是可以使用this.field访问当前实例的字段。this指示隐式参数,也就是所构造的对象。
public void setName(String name) {
this.name = name;
// 前面的this不可少,少了就变成局部变量name了
}
为什么使用this: 当我们编写类中的方法(功能)时,还没有产生对象(对象是后期产生的),而该函数需要调用本类中的对象时,就用this来表示这个对象。
this代表哪个对象: 哪个对象调用this所在的方法,this就代表哪个对象。且在同一类中可以省略this关键字。
this的应用:当我们编写类中的方法时,该方法内部要用到调用该方法的对象时,就用this来表示这个对象。
public boolean compare(Person p)
{
return this.age==p.age;
}
boolean b = p1.compare(p2);
由于是p1在调用compare方法,所以this代表p1。p2被传入参数,所以是p1和p2在作比较。所以但凡本类功能内部使用了本类对象,(compare方法中使用了p1)都需要用this表示。
10.方法参数:
1)基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。即方法不能修改基本数据类型的参数。
2)引用类型参数的传递,接收方的参数变量是调用方的变量的一个副本,都指向同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象),方法结束后副本参数变量不再使用。即方法可以改变对象参数的状态。
3)方法不能让一个对象参数引用一个新的对象。
public static void swap(Employee x,Employee y)
{
Employee temp = x;
x = y;
y = temp;
}
var a = new Employee("Alice",...);
var b = new Employee("Bob",...);
swap(a,b);//这个方法无法改变存储在变量a,b间的对象引用
因为swap方法只是交换的x,y副本的引用,而方法结束时,x,y被丢弃。原来的变量a和b仍然引用这个方法调用之前所引用的对象。
二、构造方法
1.重载:如果多个方法有相同的名字,不同的参数,便是出现了重载。因此,要完整的描述一个方法,需要指定方法名以及参数类型,这叫做方法的签名。返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却有不同返回类型的方法。
方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。
2.要想使用对象,首先必须构造对象,并指定其初始状态,然后对对象应用方法。在Java中,要使用 构造器(构造函数) 构造新实例。
3.构造方法能在创建Person实例的时候,一次性传入name和age,完成初始化。
public class Main {
public static void main(String[] args) {
Person p = new Person("Xiao Ming", 15);
System.out.println(p.getName());
System.out.println(p.getAge());
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) { //构造器
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
4.构造方法的特点:
1)构造方法与类同名且无返回值。
2)每个类可以有一个以上的构造方法,每个构造方法可以有多个参数(重载)。
class Person()
{
person(){}
}
class Person(String[] name)
{
person(){}
}
class Person(String[] name,int a)
{
person(){}
}
3)和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须使用new操作符(创建实例并做相应的初始化)。
5.如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,并将所有实例字段设置为默认值:
class Person {
public Person() {
}
}
如果我们自定义了构造方法,那么编译器就不再自动创建默认构造方法。如果没有对应的构造方法,则不能创建相应对象。我们需要自己定义需要使用的构造方法。
6.默认与显示字段初始化:
默认字段初始化:如果没有在构造方法中显示地为字段设置初值,则就会被自动地赋为默认值。数值类型的是0,布尔类型值是false,引用类型的字段默认是null。
class Person {
private String name; // 默认初始化为null
private int age; // 默认初始化为0
public Person() {
}
}
显示字段初始化:可以直接在类定义中为任何字段赋值。
class Person {
private String name = "Unamed";
private int age = 10;
}
在执行构造器之前先完成赋值操作。如果一个类的所有构造器都希望把某个特定的实例字段设置为同一个值,这个语法就特别有用。
那么如果既对字段进行初始化,又在构造方法中对字段进行初始化:
class Person {
private String name = "Unamed";
private int age = 10;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
构造方法的代码由于后运行,所以,new Person("Xiao Ming", 12)
的字段值最终由构造方法的代码确定。
7.调用另一个构造方法:一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…):
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this(name, 18); // 调用另一个构造方法Person(String, int)
}
public Person() {
this("Unnamed"); // 调用另一个构造方法Person(String)
}
}
8.初始化块(构造代码块)
初始化块是给所有对象进行统一初始化,只要构造这个类的对象,这些块就会被执行。它是完成不同对象共性的初始化内容,而构造方法是给对应的对象进行初始化。
{
cry(); //类中单独写一个哭的方法
}
首先运行初始化块,然后才运行构造器的主体部分。一般会直接将初始化代码放在构造器中,且总是将初始化块放在字段定义之后。
9.构造代码块和构造方法的区别:
1)构造代码块:用于给所有对象初始化,每创建一个新的对象都会执行一次。
2)构造函数:用于给对应对象初始化。且构造代码块先于构造函数执行。
10.调用构造器的具体处理步骤:
1)如果构造器的第一行调用了另一个构造器,则基于所提供的参数执行第二个构造器。
2)a.所有数据字段初始化为其默认值。
b.按照在类声明中出现的顺序,执行所有字段显式初始化方法和初始化块。
3)执行构造器主体代码。
三、静态字段与静态方法
1.在一个class中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。还有一种字段,是用static修饰的字段,称为静态字段:static field。
class Person {
public String name;
public int age;
// 定义静态字段number:
public static int number;
}
2.意义:每个对象共有的属性,如果随着对象存储于堆内存中,则太浪费存储空间,此时使用static关键字。static关键字是一个修饰符,修饰内容为对象所共享,可以直接被类名调用(多了一种调用方式:类名.静态成员),被存放于方法区。此时对象只需访问方法区中的修饰成员即可。
使用静态时,我们必须要搞清楚哪些是对象所共有的而哪些是对象所特有的。特有内容随对象存储在堆内存中;而共有内容被静态所修饰,存放于方法区。
3.对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例。因为虽然实例可以访问静态字段,但是它们指向的其实都是Person class的静态字段。不推荐用实例变量.静态字段去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段来访问静态对象。
4.静态常量:
public static final double PI = 3.1415926;
5.静态方法:用static修饰的方法称为静态方法。调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。
public class Main {
public static void main(String[] args) {
Person.setNumber(99);
System.out.println(Person.number);
}
}
class Person {
public static int number;
public static void setNumber(int value) {
number = value;
}
}
6.静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。
静态方法经常用于工具类。例如:
Arrays.sort()
Math.random()
静态方法也经常用于辅助方法。Java程序的入口main()也是静态方法。
public:该函数访问权限最大。
static:主函数随着类的加载就已经存在
void:主函数没有具体的返回值
main:不是关键字,但可以作为一个特殊的单词被jvm识别
String[] args:函数参数为字符串类型的数组(args是数组的名称,可以改变,但是现在已经形成一种固定写法)
我们可以改变主函数的参数列表:
public static void main(String[] args,int a)
这样只是表明主函数被重载了,但虚拟机只会寻找参数列表为字符串类型的数组作为程序入口。
7.static关键字的特点:
1)随着类的加载而加载,随着类的消失而消失。
当我们建立好一个类的时候,被静态所修饰内容就已经存在于方法区。而此时对象并未建立,也就是说,现在类中只存在静态变量(即类变量),而不存在实例变量,因此静态变量可以被类名调用。又因为它随着类的加载而加载,类的消失而消失,所以它的生命周期最长。
2)优先于对象存在。
3)可以被所有对象所共享。
4)可以直接被类名调用。
8.实例字段与静态字段的区别:
1) 存放位置:
静态字段随着类的加载而存在于方法区中。
实例字段随着对象的建立而存在于堆内存中。
2) 生命周期:
静态字段生命周期最长,随着类的消失而消失。
实例字段生命周期随着对象的消失而消失。
所以,我们只将每个对象的共性内容使用静态,因为静态字段生命周期最长,如果全部使用静态字段,则会占用大量存储空间。
9.使用静态
1)静态字段的使用:当对象中出现共享数据时,该数据被静态所修饰。
2)静态方法的使用:当功能内部没有访问到非静态数据(对象的特有数据)时,该功能可以被定义为静态的。
因为没有访问对象特有数据,那么就没有建立对象的意义,直接通过类名调用即可。
10.静态的应用
每一个应用程序中都有共性的功能,可以将这些功能进行抽取并独立封装成一个工具类,以便复用。
又因为这个工具类的对象并未封装特有数据,且操作对象的方法也没有使用对象中的特有数据,所以可以将工具类中的方法都定义为静态的,直接通过类名调用即可。但该类还是可以被其它类建立对象,所以我们可以通过对构造函数私有化的方式,强制不让它建立对象。
且工具类中,我们只暴露出我们想暴露出让用户使用的功能,其它不用用户知道的方法,可以通过将它私有化来进行隐藏。用户只需要阅读API用户帮助文档,知道如何使用即可。java中也有JDK API 帮助文档,当我们需要使用java中提供的方法的时候,就需要对API文档进行查阅。
11.静态代码块
格式:
static
{
var generator = new Random();
nextId = generator.nextInt(10000);
//Random() 构造一个新的随机数生成器
//int nextInt(int n) 返回一个0~n-1之间的随机数
}
如果类的静态字段需要很复杂的初始化代码,则可以使用静态的初始化块,它用于给类进行初始化,随着类的加载而执行,且只执行一次。
四、对象的实例化与成员调用过程
1.对象实例化
eg.Person p = new Person("zhangsan",20);
1)因为new用到了Person.class文件,所以会先找到Person.class文件并加载到内存中。
2)由于静态优先于对象,所以会执行方法区中的static初始化块,给Person类初始化。
3)在堆内存中开辟空间,分配内存地址。
4)在堆内存中建立对象的特有属性,并进行默认初始化。
5)对属性进行显示初始化。
6)对对象进行构造代码块初始化。
7)对对象进行对应的构造方法初始化。
8)将内存地址指向栈内存中的参数变量(p)。
2.对象的成员调用过程
当完成对象实例化后,调用setName方法:p.setName("LiSi");
普通方法和静态方法都存在于方法区,当调用setName方法时:
public void setName(String name)
{
this.name = name;
}
setName有局部变量name,所以会在栈内存中开辟一片空间。因为非静态肯定对应所属,所以栈内存中存在this,又因为p对象调用setName方法,所以p=this,即把p的地址赋给this,所以setName方法也指向对象,并能够修改name的值。而setName方法运行完之后,栈内存中的空间即被释放。
3.静态方法之间的调用:不访问堆内存,只走静态区数据。所以当两个静态方法都属于这个类时,静态方法之间的调用省略类名,直接写方法名。
五、访问修饰符和包
1.包:Java允许使用包(package)将类组织在一个集合中。借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。使用包的主要原因是确保类名的唯一性,如果两个程序员建立了相同类名的类,只要将这些类放置在不同的包中,就不会产生冲突。
2.为了保证包名的绝对唯一性,要使用一个因特网域名(唯一)以逆序的形式作为包名,然后对于不同的工程使用不同的子包。如:域名horstmann.com,则包名为com.horstmann。若工程名为corejava,且将Employee类放在这个包里,则这个类的完全限定名为:com.horstmann.corejava.Employee。一个类总是属于某个包,类名只是一个简写,真正的完整类名是包名.类名。
3.在定义class的时候,我们需要在第一行声明这个class属于哪个包。
package ming; // 申明包名ming
public class Person {
}
嵌套的包之间没有任何关系,如:java.util包和java.util.jar包毫无关系。每一个包都是独立的类的集合。没有定义包名的class,它使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法。
4.在包中增加类:我们必须把源文件放到与完整包名匹配的子目录中。如:com.horstmann.corejava包中所有源文件就应该放在子目录com/horstmann/corejava中。即所有Java文件对应的目录层次要和包的层次一致。编译后的.class文件也需要按照包结构存放。如果使用IDE,把编译后的.class文件放到bin目录下。
编译器编译源文件时不检查目录结构,如果它不依赖于其他包,则可以通过编译。但如果包与目录不匹配,虚拟机就找不到类。
javac com/mycompany/PayrollApp.java
java com.myconpany.PayrollApp
编译器处理文件(带有文件分隔符和扩展名.java的文件),而java解释器加载类(带有.分隔符)。
5.类的导入:一个类可以使用所属包中的所有类,以及其他包中的公共类。我们可以采用两种方式访问另一个包中的公共类:
1)使用完全限定名:
java.time.LocalDate today = java.time.LocalDate.now();
//静态工厂
2)使用import语句导入一个特定的类或者整个包。import语句应该位于源文件的顶部,但位于package语句的后面。
import java.time.*;
import java.time.time.LocalDate.now();
需要注意的是,只能用星号导入一个包,而不能使用import java.*或import java.星.星,导入以java为前缀的所有包。
6.当产生命名冲突的时候,如:java.util和java.sql包都有Date类。
1)当程序中只使用一个Date类的时候,可以使用一个特定的import语句解决:
import java.util.Date;
2)如果这两个Date类都需要使用,则在每个类名前面加上完整的包名。
var deadline = new java.util.Date();
var today = new java.sql.Date(..);
在包中定位类是编译器的工作,类文件中的字节码总是使用完整的包名引用其他类。
7.静态导入(很少使用):
package main;
// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
// 相当于调用System.out.println(…)
out.println("Hello, world!");
}
}
9.Java编译器最终编译出的.class文件只使用完整类名,因此,在代码中,当编译器遇到一个class名称时:
如果是完整类名,就直接根据完整类名查找这个class;
如果是简单类名,按下面的顺序依次查找:
1)查找当前package是否存在这个class;
2)查找import的包是否包含这个class;
3)查找java.lang包是否包含这个class。
4)如果按照上面的规则还无法确定类名,则编译报错。
// Main.java
package test;
import java.text.Format;
public class Main {
public static void main(String[] args) {
java.util.List list; // ok,使用完整类名 -> java.util.List
Format format = null; // ok,使用import的类 -> java.text.Format
String s = "hi"; // ok,使用java.lang包的String -> java.lang.String
System.out.println(s); // ok,使用java.lang包的System -> java.lang.System
MessageFormat mf = null; // 编译错误:无法找到MessageFormat: MessageFormat cannot be resolved to a type
}
}
编写class的时候,编译器会自动帮我们做两个import动作:
1)默认自动import当前package的其他class;
2)默认自动import java.lang.*;
10.访问修饰符:我们经常看到public、protected、private这些修饰符。在Java中,这些修饰符可以用来限定访问作用域。Java内建的访问权限包括public、protected、private和package权限。
1)定义为public的class、interface可以被其他任何类访问。
package abc;
public class Hello {
public void hi() {
}
}
package xyz;
class Main {
void foo() {
// Main可以访问Hello
Hello h = new Hello();
h.hi();
//hi()方法是public,可以被其他类调用,前提是首先要能访问Hello类
}
}
定义为public的field、method可以被其他类访问,前提是首先有访问class的权限。
2)定义为private的field、method无法被其他类访问,只能由定义他们的类使用。推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法:
package abc;
public class Hello {
public void hello() {
this.hi();
}
private void hi() {
}
}
3)protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类:
package abc;
public class Hello {
// protected方法:
protected void hi() {
}
}
package xyz;
class Main extends Hello {
void foo() {
Hello h = new Hello();
// 可以访问protected方法:
h.hi();
}
}
4)包访问:包作用域是指一个类允许访问同一个package的没有public、private修饰的class,以及没有public、protected、private修饰的字段和方法。
package abc;
// package权限的类:
class Hello {
// package权限的方法:
void hi() {
}
}
package abc;
class Main {
void foo() {
// 可以访问package权限的类:
Hello h = new Hello();
// 可以调用package权限的方法:
h.hi();
}
}
5)局部变量:在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。
6)final:final修饰符不是访问权限,但它可以修饰class、field和method。
- 用final修饰class可以阻止被继承
- 用final修饰method可以阻止被子类覆写
- 用final修饰field可以阻止被重新赋值
- 用final修饰局部变量可以阻止被重新赋值
package abc;
public class Hello {
protected void hi(final int t) {
t = 1; // error!
}
}