- 参考书:《Java语言程序设计与数据结构(基础篇)》—— 梁勇
- 参考视频教程:java教程
文章目录
一、类和对象相关概念
1. 面向过程的程序设计
-
面向过程的程序 = 算法+数据结构
-
程序由
全局变量
和函数
构成,用函数操作数据结构 -
不足:
- 函数和其操作的数据结构没有直观的联系,程序长了之后,难以直接看出
- 某个数据结构有哪些函数可以对它操作操作
- 某个函数到底是操作哪些数据结构的
- 任意两个函数间存在怎样的调用关系?
- 没有“封装”和“隐藏”的概念,要访问某个数据结构中的某个变量,可以直接访问。当变量定义变化时,就必须找到所有调用处修改,难以检查哪个函数导致错误
- 难以进行代码重用
- 函数和其操作的数据结构没有直观的联系,程序长了之后,难以直接看出
-
C语言就是典型的面向过程编程语言,只能用面向过程的思想进行开发
2. 面向对象的程序设计
- 面向对象的程序 = 类+类+…+类
- 设计方法:
- 把某类事物的客观属性(共同特点)归纳出来,形成一个数据结构(可以用多个变量描述事物的属性)
- 把这类事物能进行的行为也归纳出来,形成一个个函数,用于操作数据结构(这一步叫
抽象
) - 然后通过某种语法形式,把数据结构和操作此结构的函数捆绑到一起,形成一个
类
,从而将数据结构和函数紧密联系起来(这步叫封装
) - 类相当于一个
模板
,对象
是类的实例,可以从一个类构造多个实例(对象)
- 抽象的结果:成员变量和成员方法
成员变量
:描述对象的状态/属性,由数据域及其值表示成员方法
:描述对象的行为,由方法定义
- 基本特点:抽象、封装、继承、多态
- java是典型的面向对象编程语言,支持面向对象的程序开发
二、java中类和对象的使用及存储
1. 创建和使用类
(1)创建类
- java中通过
class
关键字创建类,在类中直接定义成员变量和成员方法,注意成员变量放在成员方法外边 - 类可以嵌套定义
内部类
:在一个类中定义的类,称为内部类(参考:浅谈Java内部类)外部类
:和内部类相对应的,不在其他类内部定义的类称为外部类
- 一个
.java
源码文件中可以定义多个类,但只能有一个public
类,且文件名必须和此public
类同名- 如
Demo.java
中必须有且只有一个public
公共类,且它叫Demo
- 编译时,java编译器把每一个类都单独生成一个
.class
文件
- 如
- 通常,我们使用IDE开发java程序时,会把每个类单独放在一个同名
.java
文件中 - 默认值:创建类后,类的成员变量会有一个默认值,规则和数组一样
(2)使用类
-
使用类的一般步骤:
-
导包,指出需要使用的类在什么位置,对于和当前类在同一个包的情况,可以省略导包语句
import 包名称.类名称 //cn.itcast.day01.demo01是包名,Student是类名 import cn.itcast.day01.demo01.Student
-
创建实例(对象):
类名称 对象名 = new 类名称(); Student stu = new Student();
-
使用
使用成员变量:对象名.成员变量名 使用成员方法:对象名.成员方法名(参数列表)
-
-
类对象是一种引用数据类型,类似C的指针指针类型,可以进行以下操作
类名 引用变量名 = new 构造函数(参数表); //简化写法 类名 引用变量名; //创建引用变量 引用变量名 = new 构造函数(参数表); //创建对象并给引用变量赋值 引用变量名_1 = 引用变量名_2; //"引用变量名_1" 指向 "引用变量名_2" 指向的对象
-
有时候,对象创建后不需要引用访问,这时可以不把他赋给某个引用变量,这样创建的对象叫
匿名对象
System.out.print(new Circle(5).getArea()); //这里的Circle对象就是匿名对象
匿名数组就是匿名对象的一种,使用匿名对象的策略仅仅就是创建、初始化、应用,因为它没有任何名字因此没法重用它
(3)说明
- 关于引用变量赋值和垃圾回收
- 假设有两个引用变量
C1
、C2
,C1 = C2;
会使C1
指向C2
指向的对象,这时如果原来C1
指向的对象没有其他引用变量引用了,那个对象就会成为不可访问的,会自动被java垃圾回收机制回收。关于对象的存储详见下文 - 如果要消除某个对象,可以给引用它的引用变量赋值
null
,这样它就会被自动回收
- 假设有两个引用变量
2. 内存中的存储情况
- 和
String
或数组相同,类对象是一种引用数据类型,这类似C中的指针类型,其存储的是一个地址值。创建类对象时,在堆区创建类对象,在栈区创建类对象变量,对象变量本质上存储着堆区对象的地址。如果堆区的某个类对象没有任何引用变量指向它,就会被垃圾回收机制自动回收
(1)堆/栈/方法区
- JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)
- 堆区:
- 存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
- jvm只有一个堆区,被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
- 通过
new
关键字,在堆区创建对象
- 栈区:
- 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象/引用数据类型的引用。例如
String str = "12345";
这行代码,它在栈区创建了一个引用str
,在堆区创建了一个String对象"12345"
,并且把str
引用到它 - 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
- 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
- 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象/引用数据类型的引用。例如
- 方法区:
- 又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
- JDK8之前,由永久代实现,主要存放类的信息、常量池、方法数据、方法代码等;JDK8之后,取消了永久代,提出了元空间,并且常量池、静态成员变量等迁移到了堆中;元空间不在虚拟机内存中,而是放在本地内存中。
- 堆区:
(2)静态成员和非静态成员
- 通过
static
关键字可以设置类成员为静态的-
成员变量
- 非静态成员变量(实例变量):是属于类的实例的,在jvm加载类的字节码文件到方法区时不创建,只有在类实例创建时才在堆区一起创建。每个类实例中只能直接访问到自己的成员变量,不能直接访问其他同类实例中的同名变量。在使用时,只能通过
实例名.变量名
的方式使用 - 静态成员变量(类变量):是属于类的,在jvm加载类的字节码文件到方法区时即创建,早期(jdk8之前)是放在方法区的,后来改为存放在堆区。所有类的实例中都可以直接访问到类的非静态成员变量,并能对其进行修改。使用时,可以
实例名.变量名
这样访问,也可以通过类名.变量名
的方式使用它,而不需要创建对象。
- 非静态成员变量(实例变量):是属于类的实例的,在jvm加载类的字节码文件到方法区时不创建,只有在类实例创建时才在堆区一起创建。每个类实例中只能直接访问到自己的成员变量,不能直接访问其他同类实例中的同名变量。在使用时,只能通过
-
成员方法
- 非静态成员方法(实例方法):在jvm加载类的字节码文件到方法区时,类的实例方法并不分配入口地址,只有当我们创建对象时,类的实例方法才会被分配访问地址。在创建类的第一个对象时,类的实例方法的入口地址就会被分配,之后再创建此对象时不再给实例方法的分配新的地址,而是共享第一个对象被创建时分配的实例方法的入口地址,并且,只有当类的最后一个实例对象被销毁时才会将地址回收。类似实例变量,只能用
实例名.方法名()
形式使用 - 静态成员方法(类方法):在jvm加载类的字节码文件到方法区时,类方法就会被分配入口地址,所以类方法的入口地址分配比实例方法要早。类似实例变量,可以用
实例名.方法名()
或类名.方法名()
两种形式使用
- 非静态成员方法(实例方法):在jvm加载类的字节码文件到方法区时,类的实例方法并不分配入口地址,只有当我们创建对象时,类的实例方法才会被分配访问地址。在创建类的第一个对象时,类的实例方法的入口地址就会被分配,之后再创建此对象时不再给实例方法的分配新的地址,而是共享第一个对象被创建时分配的实例方法的入口地址,并且,只有当类的最后一个实例对象被销毁时才会将地址回收。类似实例变量,只能用
-
(3)静态代码块
- 静态代码块是类中使用
static
关键字修饰的代码块。 - 格式
public class 类型名{ static { // 静态代码块内容 } // 构造方法.... // 成员方法.... }
- 特点:
- 第一次用到本类时,静态代码块执行唯一的一次
- 静态内容总是优先于非静态内容,所以静态代码块优先于构造方法执行
- 主要用途:一次性地对静态成员变量进行赋值,在涉及jdbc时,静态代码块很有用
3. 示例
(1)单一实例的情况
-
在package
cn.itcast.day01.demo02
中建立一个Phone
类package cn.itcast.day01.demo02; public class Phone { String brand; //品牌 double price; //价格 String color; //颜色 public void call(String who){ System.out.println("给"+who+"打电话"); } public void sendMessage(){ System.out.println("群发短信"); } }
-
在同一个package
cn.itcast.day01.demo02
中建立一个DemoPhone
类,其中包含main
方法。由于和Phone
类在同一个package中,所以不需要导包package cn.itcast.day01.demo02; public class DemoPhone { public static void main(String[] args) { Phone one = new Phone(); System.out.println(one.brand); //null System.out.println(one.price); //0.0 System.out.println(one.color); //null one.brand = "苹果"; one.price = 8388.0; one.color = "黑色"; System.out.println(one.brand); //苹果 System.out.println(one.price); //8388.0 System.out.println(one.color); //黑色 one.call("乔布斯"); //给乔布斯打电话 one.sendMessage(); //群发短信 } }
-
内存示意图如下
- 可见
- 类信息存储于方法区;类实例存储于堆区;
- 在运行过程中,每次方法调用会在栈区建立一个栈帧,程序转至此栈帧运行,方法运行完毕后其栈帧弹出
- 类的实例变量保存的是堆中类对象的地址,从而可以通过实例变量引用到对象
(2)两个同类实例的情况
-
修改
DemoPhone
,增加一个同类实例,如下package cn.itcast.day01.demo02; public class DemoPhone { public static void main(String[] args) { Phone one = new Phone(); one.brand = "苹果"; one.price = 8388.0; one.color = "黑色"; System.out.println(one.brand); //苹果 System.out.println(one.price); //8388.0 System.out.println(one.color); //黑色 one.call("乔布斯"); //给乔布斯打电话 one.sendMessage(); //群发短信 //--------------------------------------- Phone two = new Phone(); two.brand = "三星"; two.price = 5999.0; two.color = "蓝色"; System.out.println(two.brand); //三星 System.out.println(two.price); //5999.0 System.out.println(two.color); //蓝色 two.call("哈哈哈"); //给哈哈哈打电话 two.sendMessage(); //群发短信 } }
-
此时内存示意图如下
- 可见,同一个类的多个实例,在第一个实例被创建时给实例方法分配入口地址,之后的其他实例共用同一个入口地址
(3)两个引用变量引用到同一实例的情况
- 再修改
DemoPhone
,two
不指向和one
相同的对象package cn.itcast.day01.demo02; public class DemoPhone { public static void main(String[] args) { Phone one = new Phone(); one.brand = "苹果"; one.price = 8388.0; one.color = "黑色"; System.out.println(one.brand); //苹果 System.out.println(one.price); //8388.0 System.out.println(one.color); //黑色 one.call("乔布斯"); //给乔布斯打电话 one.sendMessage(); //群发短信 //--------------------------------------- Phone two = one; two.brand = "三星"; two.price = 5999.0; two.color = "蓝色"; System.out.println(two.brand); //三星 System.out.println(two.price); //5999.0 System.out.println(two.color); //蓝色 two.call("哈哈哈"); //给哈哈哈打电话 two.sendMessage(); //群发短信 } }
- 此时内存示意图如下
三、类和方法
1. 类作为方法的参数
- java中只有一种传参数方法:
值传递
- 对于引用型变量(String/数组/类对象…)等,传递的是地址值
- 示例
- 这个示例中,我们假设堆中类对象的地址为
0x666
,在main方法中创建的对象变量one
保存了0x666
这个地址,从而可以引用到堆中的对象。当one
作为实参传入方法method
时,由于是值传递,形参param
也保存了0x666
,从而可以访问到堆中的对象
- 这个示例中,我们假设堆中类对象的地址为
2. 类作为方法的返回值
- java中只有一种传返回值方法:
值传递
- 对于引用型变量(String/数组/类对象…)等,传递的是地址值
- 示例
- 这个示例中,我们假设堆中类对象的地址为
0x666
,在getPhone
方法中创建的对象变量one
保存了0x666
这个地址,从而可以引用到堆中的对象。当one
作为返回值赋值给main方法中的对象变量two
时,由于是值传递,two
也保存了0x666
,从而可以访问到堆中的对象
- 这个示例中,我们假设堆中类对象的地址为
四、成员变量和局部变量的区别
-
定义的位置不同
- 局部变量:在方法内部
- 成员变量:在方法外部,直接写在类中。类变量只能声明一次,但在类方法中可以出现和类变量同名的局部变量(可以是多个),且方法中局部变量优先
-
作用范围不一样
- 局部变量:只有方法内部可以使用,出了方法就不能用了
- 成员变量:整个类全部可以通用
-
默认值不一样
- 局部变量:没有默认值,如果要想使用,必须手动赋值
- 成员变量:有默认值,规则和数组一样
-
内存位置不一样
- 局部变量:栈内存
- 成员变量:堆内存
-
生命周期不一样
- 局部变量:随着方法进栈而诞生,随着方法出栈而消失
- 成员变量:随着对象创建而诞生,随着对象回收而消失
五、构造方法
- 构造方法是专门用来创建对象的方法,在用
new
创建对象时,就是在调用构造方法 - 格式:
public 类名称(参数列表){ 方法体 }
- 注意:
- 构造方法的名称必须和所在类的名称完全一样(包括大小写)
- 构造方法不要写返回值类型(void也不要写),且方法中不能有
return
- 如果没有手动写出构造方法,编译器会隐式定义并使用空构造方法,称为
默认构造方法
,形如public 类名称(){}
,它什么也不做 - 一旦编写了至少一个构造方法,编译器不会生成使用空构造方法了
- 构造方法可以进行重载
六、一个标准的类(Java Bean)
1. private关键字
-
使用
private
关键字修饰的成员成为类的私有成员,在本类中可以随意访问,但在类外不能通过示例名,成员名
的形式直接访问了 -
对于每个私有成员变量,定义一对
setter
和getter
方法,间接对其进行设置和访问。- 通过这种方式,可以对数据进行一些检查和预处理,可以提高代码的安全性
- 一般将
setter
和getter
方法命名为getXxx()
和setXxx()
。对于boolean
,其getter
方法一般命名为isXxx()
-
示例
-
定义一个person类
package cn.idcast.day04.demo03; public class Person { String name; private int age; // 私有成员 public void show(){ System.out.println("我叫"+name+",年龄"+age); } // 定义一对getter和setter方法,设置/获取私有成员 public void setAge(int num) { if(num<100 && num>=0) // 在setter中,可以对数据进行检查 age = num; else System.out.println("数据不合理"); } public int getAge() { return age; } }
-
在main方法中,通过
setter
和getter
处理私有成员变量package cn.idcast.day04.demo03; public class Demo03 { public static void main(String[] args) { Person person = new Person(); person.name = "Bill"; person.setAge(-20); person.show(); } }
-
2. this关键字
this
是一个对象用来引用自身的引用名,可以引用对象的属性和方法- 通过谁调用的方法,谁就是
this
。比如在main
中有Person
类实例student
,执行student.setAge()
调用时,setAge()
方法内部的this
就指代student
(他们保存了同一个堆中Person
类对象的地址) this
的使用场景-
this
通常可以省略//假设有属性 private radius; this.radius = 1; //等价于radius = 1;
-
当方法的局部变量和类的成员变量同名时,根据 “就近原则” ,优先使用局部变量。如果要在方法中访问同名的类成员变量,可以通过
this.成员变量名
的形式// 假设有属性 private radius; public void setR(int radius) { this.radius = radius; //this.radius引用了类属性 }
-
this
用来调用同一个类的其他构造方法public class Circle { private double r; //构造函数1 public Circle(double r) { this.r = r; } //构造函数2 public Circle() { this(1.0); // 这里调用了构造函数1 ... // java要求,构造方法中的this(arg-list)调用应在其他可执行语句前出现,所以其他指令...应在这里 } }
当一个类有多个构造方法时,推荐手动实现参数最多的构造,其他参数少的构造用this调用参数最多的来实现
-
3. Java Bean
-
一个标准的类应该包含以下四个部分
- 所有成员变量使用
private
关键字修饰 - 每个成员变量编写一对
setter
和getter
方法 - 编写一个无参数构造方法
- 编写一个全参数构造方法
- 所有成员变量使用
-
这样的一个标准类称为
Java Bean
-
在IntelliJ IDEA环境中,写完成员变量后,可以通过
菜单栏code -> Generate
自动生成getter
和setter
以及构造方法Constructor
七、不可变类
- 通常创建一个对象,其内容是允许改变的
- 可以利用 “不可变类” 构造 “不可变对象”, 其内容不允许改变。例如
String
类就是不可变的(String
相关操作中,都是根据初始化字符串返回新字符串,不能修改String
的初始化值) - 一个不可变类具有以下特点
- 其所有数据域都是
private
的 - 没有可以设置数据域的方法(没有
setter
方法) - 没有返回一个指向数据域的引用的访问器方法
- 其所有数据域都是