类(Class)和对象(Object)是面向对象的核心概念
- 类 是对一类事物的描述,是抽象的、概念上的定义
对象 是实际存在的某一类事物中的个体,因此也称为实例
面向对象的重点就是设计类,设计类的重点就是设计类中的成员
1. 类
1.1 类基础
类有两大属性,属性和行为,属性对应类中的成员变量,行为对应类中的成员方法。
Field = 属性 = 成员变量,Method = (成员)方法 = 函数
// 一个简单的类定义
class Person{
// 属性
String name;
int age;
boolean isMale;
// 方法(函数)
public void eat (){
System.out.println("吃饭");
}
public void sleep (){
System.out.println("睡觉");
}
public void code (String language){
System.out.println("写代码,用"+language);
}
}
1.2 属性vs局部变量
【相同点】
- 定义的格式一致
- 都是先声明后使用
- 变量都有对应的作用域
【不同点】
- 声明位置不同
- 属性:直接定义在类的{ }中
- 局部变量:声明在方法、方法形参、代码块、构造器形参、构造器内部的变量
- 权限修饰符不同
- 属性:可以在声明属性时,使用权限修饰符指明其权限,常用的有 private、public、缺省、protected
- 局部变量:不可以使用权限修饰符
- 默认初始化值不同
- 属性:根据其类型,都有默认的初始化值,和数组元素的初始化值一样
- 局部变量:没有默认初始化值
- 内存结构不同
- 属性:在堆中
- 局部变量:在栈中
1.3 构造器
构造器(constructor),顾名思义,就是通过传参的方式来构造一个属性有初始值的实例对象。不同于其他方法,构造器总时结合new操作符来调用。
构造器必须与类同名,当一个类含有构造器时,创建对象时必须使用该构造器来创建,并传入相关的参数。在前文中,使用 Perosn p1 =new Person( ) 来创建是因为Person中没有构造器,所以括号内不用传入任何参数。当一个类没有定义构造器时,会默认产生一个无参的构造器,其权限和所属类的权限一致,类是public构造器就是public,类是缺省构造器就是缺省。
现在如下定义一个含有构造器的类:
class Person {
// 属性
public String name;
public int age;
boolean isMale; // 没有权限修饰符,默认缺省
private double salary ;
// 构造器
public Person(String n, int a, boolean b){
name = n ;
age = a ;
isMale = b;
}
// 方法
public void eat() {
System.out.println("吃饭");
}
}
当一个类包含带参数的构造器后,就不能用空括号来new一个对象了。
使用构造器创建对象并用参数初始化相应属性:
构造器是非常常见的,可以用构造器来创建一个对象数组,每一个数组元素都是初始值不同的对象。
Person[] persons = new Person[3]; // 先new一个数组,元素类型为Person
persons[0] = new Person("lisi",21,true); // 对每一个元素都new一个对象
persons[1] = new Person("wangwu",22,true);
persons[2] = new Person("zhaoliu",19,false);
一个类中可以有多个构造器,每个构造器都要与类同名,但是参数不一样,调用时只需区别构造器的参数即可。
class Person {
// 属性
public String name;
public int age;
boolean isMale; // 没有权限修饰符,默认缺省
private double salary ;
// 构造器1
public Person(String n, int a, boolean b){
name = n ;
age = a ;
isMale = b;
}
// 构造器1
public Person(String n, int a){
name = n ;
age = a ;
}
}
public class test {
public static void main(String[] args) {
Person[] persons = new Person[3]; // 先new一个数组,元素类型为Person
persons[0] = new Person("lisi",21,true); // 对每一个元素都new一个对象
persons[1] = new Person("wangwu",22);
}
}
总结:
- 构造器与类同名
- 每个类可以有一个或以上的构造器
- 构造器可以设置0个、1个或多个参数
- 构造器没有返回值
- 构造器总是结合 new 一起使用
1.3 权限
public 指该属性/方法可以被所有的类访问,private 指该属性/方法无法被其他类访问。当Person类中一个属性A被定义为private时,在Test类中创建Person实例就无法访问A了。
在一个类中,public修饰的属性和方法可以由任意包中的任意类访问,private修饰的属性和方法只能被定义它的类访问,而没有修饰符的属性和方法为缺省状态,可以由其所在包中的任意类访问。
而 protected 范围要比 private、缺省要大,比 public 小,在不同包的子类下也可以访问。四种权限修饰符的范围如下表所示:
修饰符 | 类内部 | 同一个包 | 不同包的子类 | 同一个工程 |
---|---|---|---|---|
private | √ | |||
缺省(default) | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
四种权限都可以修饰类的内部结构:属性、方法、构造器、内部类,但是类只能被两种权限修饰符修饰:缺省和public
当构造器使用 private 时,创建对象时会无法使用该构造器,所以没什么意义
class Person {
private String name;
private static int age = 1;
private Person(String n){
name = n;
}
}
public class test {
public static void main(String[] args) {
final Person p1 = new Person(); // ERROR!
final Person p1 = new Person("zhangsan"); // ERROR!
}
}
在实现一个类时,公共的数据非常危险,破坏这些数据的捣乱者可能出现在任何地方,放项目工程量大的时候,会很难调试,因此应该将所有的属性都设置为私有的。
当属性设置为私有后,其他类就无妨访问该属性,甚至连读都不行,这显然是不符合业务逻辑的。因此,若想获得或者设置某个对象的属性并且保证安全性,一般需要提供以下三项内容:
- 一个私有的属性
- 一个公共的属性访问器方法
一个公共的属性更改器方法 ```java class Person { // 属性 private String name; private int age; private boolean isMale; // 没有权限修饰符,默认缺省 private double salary ;
// 获取name public String getName(){
return name;
}
// 设置name public void setName(String n){
name = n;
}
}
public class test { public static void main(String[] args) { Person p1 = new Person(); p1.setName(“zhangsan”); System.out.println(p1.getName()); } }

<a name="Vj2qo"></a>
## 1.4 final
将类中属性设为final,表示在每一个构造器执行后,这个属性的值已经设定且**无法更改**,比如将Perosn类中的name设为final,那么在创建一个Person对象后该对象的name就无法更改,也即无 setName 方法,说白了就是动态的常量。
```java
class Person{
private final String name;
private double salary;
}
如果类中的所有方法都不会改变其对象,这样的类就是不可变的类,比如String就是不可变类。
当final作用于一个对象时,代表这个对象变量不可以再应用其他对象变量,但是这个对象内部的属性可以被更改:
【总结】
- 如果引用为基本数据类型,则该引用为常量,该值无法修改;
- 如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
- 如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
- 当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
- 当用final修改类时,该类成为最终类,无法被继承。简称为“断子绝孙类”。
1.5 static 属性和方法
1.5.1 静态属性
对于非静态属性而言,每个对象都有自己的一个副本,在内存中也有独自的空间。但是 static 将让一个类的所有对象共享一个空间。
现在,每一个Person对象都有一个自己的 id 属性,1000个对象就有1000个id。然而这1000个对象将共享一个 nextId 字段,共用这一片内存。并且,即使没有Person实例被创建,静态属性 nextId 也存在。它属于类,而不属于任何单独的对象。class Person{ private int id ; private static int nextId = 1 ; // 定义时即初始化 }
【静态常量】
静态变量使用的少,但是静态常量却非常常用,用 static + finall 定义一个静态常量,由于静态常量无法被修改,所以权限给 public 就行。
public class Math{
public static final double PI = 3.1415926535;
}
1.5.2 静态方法
静态方法是不在对象上执行的方法。例如 Math 类中的 pow 方法就是一个静态方法,它通过类直接调用,而不能通过任何创建的Math对象变量来调用。
Math.pow(x,a); // x^a
换句话说,静态方法是没有隐式参数的(对象本身)
由于静态方法不能在对象上执行,所以静态方法不能访问任何非静态属性,他只能访问静态属性。
可以访问静态属性
正是因为静态方法不需要任何对象,而程序启动时也没有建立任何对象,因此所有的 mian 方法都需要 static 修饰为静态方法,然后在 mian 方法体的执行过程中创建程序所需要的对象。
public class test {
public static void main(String[] args) {
// .....
}
}
2. 对象
2.1 对象基础
定义完类后,需要对类进行实例化处理,就是对象。因为类是引用类型,所以实例化时需要用到 new。实例化完成后,就可以使用该实例中的所有属性和方法
public class test{
public static void main (String[] args){
Person p1 = new Person();
// Scanner scan = new Scanner(System.in);
// 调用对象的结构:属性、方法
// 调用属性:
p1.name = "TOM";
String name2 = p1.name;
p1.isMale = true;
System.out.println(p1.name);
// 调用方法:
p1.eat();
p1.code("Java");
p1.sleep();
}
}
如果创建了一个类的多个对象,则每一个对象都独立的拥有一套类的属性(如果属性非static)。意味着对一个对象的属性A进行操作将不影响另一个对象的属性A。
【对象赋值】
如果将一个对象赋值给另一个对象,实际上是将对象的地址赋过去,会使二者指向堆空间中的同一个实体,此时,调用p1,p2就是调用同一个对象实体,相互影响。、
2.2 对象的内存解析
p1和p2是各自独立创建的对象,都有各自的堆空间,但是p3不是,p3与p1共享一个堆空间
很多人错误地把Java中的对象变量理解为C++中的引用。然而,在C++中没有null引用,而且引用不能赋值。实际上,可以把Java中对象变量看作C++的对象指针。例如
Person p1 = new Person();
相当于
Person* p1 = new Person();
因为C++的对象指针只有使用了new调用才会初始化,就这一点而言,Java和C++的语法几乎是一样的。在C++中,指针很容易出错,比如创建了一个野指针或空指针,这点在Java中是不存在的。同时,Java中不必担心内存管理的问题,垃圾回收机制会处理相关的错误,后续会学到。
2.3 重载
不同与C,Java中一个类的多个方法是可以重名的,典型的例子就是构造器。除了构造器,其他方法也是这样的。
var messages = new StringBuilder(); // var用来自动确定类型
var todoList = new StringBUilder("To do:\n");
上述情况就称作重载,如果多个方法(比如构造器)有相同的名字、不同的参数,就出现了重载。编译器必须挑选出具体调用哪个方法。它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法。
Java允许重载任何方法,因此,要区分重名的不同方法,需要指定方法名以及参数类型,这叫做方法的签名
indexOf(int);
indexOf(int,int);
indexOf(String);
// 四个不同签名
2.4 this 关键字
先来看一个问题:
class Person {
// 属性
private String name;
private int age;
// 设置age
public void setAge(int age){
if (age >= 0){
age = age ; // 编译并不会报错
}
}
}
在上述类中,setAge的形参和Persong中的一个属性同名,编译虽然不会报错,但是运行逻辑会出了问题,代码秉承就近原则,在 age = age ; 处,将前后两个 age 都看成了 setAge的形参age。解决该问题的方法就是关键字 this。
this 指当前对象,可以用来修饰 属性、方法、构造器,this.age 代表当前对象的 age 属性,所以上述代码可以更改为:
class Person {
// 属性
private String name;
private int age;
// 设置age
public void setAge(int age){
if (age >= 0){
this.age = age ; // √
}
}
}
this 还可用于调用构造器,因为 this 相当于当前类(或是对象)的代名,所以 this( ) 就相当于调用了当前类下的构造器,其中()内的参数取决去调用哪一个构造器,如果是无参构造器,就是空。
上述代码中,类Person定义了两个构造器,在第二个构造器里使用 this( ) 来调用第一个构造器。因此,在外界类使用 new Person(String name) 来创建对象时,由于 this( ) 的存在会在指定地方进入构造器 Person( ),完成后再返回原构造器执行,最终相当于一次执行了 this.eat( ) + this.name = name ;
3. 包
Java允许使用包(package)将类组织在一个集合中。借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。只要将同名的类放置在不同的包中,就不会出现冲突。
3.1 包命名
【命名规范】
- 数字不能开头
- 字母都要小写
- 包名与路径对应,从项目的 src 下开始。
- 每“.”一次就代表一层文件目录
3.2 类的导入
一个类可以使用所属包的所有类,以及其他包中的公共类(public class),这点和Go一样!!
在一个类中,public修饰的属性和方法可以由任意包中的任意类访问,private修饰的属性和方法只能被定义它的类访问,而没有修饰符的属性和方法为缺省状态,可以由其所在包中的任意类访问。
用 import 来引用包中的各个类,一旦import完成,在使用类时就不必写出类的全名了。
import java.util.Scanner ; // util就是包
public class test {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int a = scan.nextInt();
}
}
如果用到的类过多,可以直接一口气导入某一包中的全部类,用通配符 *
import java.util.* ;
但是,需要注意的是,只能使用 * 来导入一个包,而不能使用 import java.* 或 import java.*.* 来导入所有以java为前缀的包。<br />大多数情况下,可以只导入需要的包,并不必过多考虑它们的类。但是在发生类命名冲突时,就要注意了。例如,java.util 和 java.sql 都含有 Date 类
解决冲突的方法有两种,一个是增加一个特定的 import 语句来解决问题:
import java.util.* ;
import java.sql.* ;
import java.util.Date ;
public class test {
public static void main(String[] args) {
Date today = new Date() ; // OK, java.util.Date
}
}
另一个方法就是在类名前加上完整的包名:
import java.util.* ;
import java.sql.* ;
public class test {
public static void main(String[] args) {
java.util.Date today = new java.util.Date();
}
}
3.3 静态导入
有一种 import 语句允许导入 静态方法 和 静态属性,而不只是类,比如:
import static java.lang.System.* ;
就可以使用System类的静态方法和属性,而不必加类名前缀
out.println("Hello!") ; // => System.out.println
exit(0) ; // => System.exit
另外,还可以导入特定的方法或属性
import static java.lang.System.out ;