06. 面向对象基础
对象和类
学习目标:
- 掌握对象与类的关系
- 掌握如何定义类
- 掌握对象的创建与使用
引例
张三养了3只狗,一只1岁白色萨摩亚,一只2岁黄白柯基,一只3岁黄色金毛
解决方案:
- 单独变量模拟
- 数组方式模拟
Code
public static void main(String[] args) {
// 单独变量解决
// 第一只dog
String dogName1 = "萨摩耶";
String dogColor1 = "白色";
int dogAge1 = 1;
// 第二只狗
String dogName2 = "柯基";
String dogColor2 = "黄白色";
int dogAge2 = 2;
// 第三只狗
String dogName3 = "金毛";
String dogColor3 = "黄色";
int dogAge3 = 3;
// 数组方式解决
String[] dog1 = {"萨摩耶", "白色", "1"};
String[] dog2 = {"柯基", "黄白色", "2"};
String[] dog3 = {"金毛", "黄色", "3"};
}
单独变量解决==》不利于数据管理
数组解决==》数据类型无法体现,取值只能通过下标,变量名跟内容很难关联起来
缺点:不利于数据管理,效率低
为了解决上述问题, 我们引入了类与对象.
对象的概念
在生活中,我们每个人都是独立的个体,还有一些客观存在的个体,个体之间通过交互共同组成了这个世界。
面向对象程序的世界就是对现实生活的模拟,就是把现实生活中的场景搬到程序中。
Java 是面向对象的编程语言,独立的个体就是,对象就是面向对象程序设计的核心。
对象之间相互协作,共同实现程序的功能。
现实中存在的个体,Ta们具有特定的属性和特定的行为,程序世界中的也类似,具有以下特点:
- 属性:个体的状态信息(数据)
- 行为:个体能够做什么(操作)
类的概念
现在我们知道了对象对程序世界的重要性,为了完成程序开发,我们程序员的工作就变成了:
创建一个个对象,并维护对象之间的交互。
在这个过程中,最基本,最先要解决的问题就是——对象的创建,怎么得到一个对象呢?
就像上帝造人,女娲造人是以自身为模板一样。我们程序员创建程序中的对象,也需要一个。
在Java中把创建对象的"模板",称之为。
也就是说,到目前为止,我们终于可以给我们之前天天用的类(class)下一个明确的定义了:
创建对象的模板就是类!类就是创建对象的模板!
既然类是模板,那么:
类就抽取了同类别的所有对象属性和行为上的,一个类可以描述千千万万个对象。
类与对象的关系
类和对象的关系:
类描述了,同种类型的对象,在属性和行为上的共性特征。
所以:
类是抽象的,而对象是具体的,所以对象也称为实例(instance)。
类只规定了共性,只是描述对象该有什么属性,该有什么行为。
但是:
具体对象属性的取值,即便是同类型对象也可能有差异。一个类可以创建出千千万万个不同的对象。
类的定义
定义类包括定义类本身,和定义类中结构两部分。
定义一个类
语法
[类修饰符列表] class 类名{
// 类体
}
定义类中的成员
类中成员分为两部分:
- 成员变量: 描述对象的共有属性
- 成员方法: 描述对象的共有行为
如何定义成员变量?
定义。
[修饰符列表] 数据类型 成员变量名;
注意:
成员变量,在整个类体中生效,在整个类中的成员方法中都可以访问它!
如何定义成员方法?
[访问权限修饰符] 返回值类型 方法名(形参列表){
// 方法体
}
解释:
- 成员方法和我们之前使用的方法不一样,必须没有static修饰!!
- 访问权限修饰符我们还未学习,这里先默认是!
- 其它诸如形参列表,方法体,方法名等结构和之前讲的方法一样。
定义一个学生类Demo
class Student{
// 属性
public String name;
public int age;
// 行为
public void study(){
}
}
对象的创建与使用
创建
创建类的对象一般需要在方法中进行,在确定能够创建对象的地方,可以使用以下语法创建对象:
类名 对象名 = new 类名();
这个语法,我们并不陌生,在Scanner键盘录入的时候,已经见过了,现在做如下解释:
- 类名表示创建是何种对象,对象的类型是什么。
- 对象名和之前讲的数组名是一个概念,都是引用数据类型的引用。作为一个变量名,对象名遵循小驼峰式的命名规范。
- new关键字表示在堆上开辟空间创建对象,注意代码中的new关键字表示一定会在堆上创建一个独立的对象。
比如创建一个Student对象,就应该这么写:
Student s = new Student();
使用
请大家思考一个问题:
如果不创建对象,能够直接访问类中的成员变量和成员方法吗?
很明显是不可以的
注意:
无论成员变量还是成员方法,都是属于对象的!必须创建对象才能访问它们!
实际上不同对象,访问类中同一个成员变量和成员方法的结果完全可能是不同的!!
对象创建出来后,大致可以做以下操作:
直接输出对象名
和数组直接输出数组名是一样,默认情况下,直接打印对象名得到的是:
- 该类的全限定类名 + "@" + 十六进制的地址值
- 可以通过==进行地址值的比较
使用对象获取对象的属性和行为:
访问属性(获取属性值和修改属性值)
语法:
数据类型 变量名 = 对象名.成员变量;
通过上述方式就可以直接获取属性值了,修改属性值也是类似的做法:
对象名.成员变量 = 值;
注:对象中的成员变量,类似于数组对象中的元素,它们都具有默认初始化和默认值!!!
而具体默认值是什么,也和数组对象中的元素一致,不再赘述!!
调用行为(方法)
语法:
对象名.成员方法名(实参);
如果方法有返回值,还可以接收返回值。
注意事项
- 类可以嵌套定义称之为内部类, 但是请现在不要嵌套定义类,一个Java文件中定义多个class应该并列而不是包含。
- 一个Java文件中的多个class是同包(文件夹)关系。
- 一个类当中,应该开门见山的定义成员变量,而后再写成员方法。
- 类中没有的属性和行为,对象是不可能有的,类是模板,模板中有才能体现在对象中。
- 使用new关键字就会创建新的对象,两条new语句创建的对象是完全独立的。
- 成员变量,在类的全局生效,不像局部变量仅在作用域内生效!成员变量,在整个类体中生效,在整个类中的成员方法中都可以访问它!
引用数据类型
学习目标:
- 了解类是一种自定义数据类型
- 了解类加载
基本概念
先回顾一下,在Java基础语法部分,我们给出的数据类型的概念/定义:
数据类型: 表示的是一组数据的集合,和基于该数据集合的一组合法操作。
那么这个定义能不能套在引用数据类型中呢?能否用数据类型的概念来统一基本数据类型和引用数据类型呢?
在类的定义中,我们知道类中的成员包括2部分:
- 成员变量
- 成员方法
成员变量本质上就是数据,成员方法本质上就是操作,那么假设做以下类比:
- 数据的集合: 类中成员变量的集合
- 操作的集合: 类中成员方法的集合
于是,就可以做出以下总结:
- 一个类的定义,实际上就是定义了一种全新的数据类型,一种自定义的数据类型。
- 这种完全不同于基本数据类型的数据类型,我们称之为"引用数据类型"。
类加载介绍
当我们在程序中使用一个基本数据类型时,由于基本数据类型是JVM当中已经预先定义好的(所以基本数据类型叫"内置数据类型"),JVM可以清楚的知道这个基本数据类型变量在内存中的存储方式(占用空间大小、结构等等),JVM能够正常开辟空间,正常给变量初始化赋值。
但是,并不是内置数据类型,而是我们自定义的数据类型。
现在我们要根据一个类来创建它的对象,要让JVM帮助我们开辟空间创建引用和对象,JVM怎么知道到底要创建什么呢?难道它未卜先知吗?
显然是不可能的,某个类在一开始并不被JVM——它不知道类中有什么,必然不可能做任何操作。
所以在对某个类做任何操作之前,都需要让JVM来这个类型。
在Java中,把JVM一个类的过程,称之为类加载
关于类加载:
- 类加载的具体过程,我们后面会详细学习。这里我们先不用了解它的详细过程。
- 类加载是通过把某个类的二进制字节码文件(class文件)通过I/O流的形式,读取进JVM内存中的方法区实现的。
- 通过读取二进制字节码文件,并加载进JVM内存。这是JVM了解这个类型的过程。
- 类加载之后,就可以做很多类型相关的操作了。
- 类加载要在创建对象之前进行,换句话说创建一个类的对象必然触发该类的类加载!
我们通过画对象内存图,来展示这一过程:
- 一个对象的内存图,一个对象的创建过程。(创建1个Student对象)
- 三个对象的内存图,其中有两个引用指向同一个对象。(创建3个Teacher对象,并进行显式赋值)
总结:
一个类的类加载在一次程序运行过程中,最多只有一次。
多个引用指向同一个对象时,某个引用修改了对象的状态(成员变量的取值),再用其它引用访问会得到修改后的结果。
注:这一点实际上和数组是一样的。
类加载IO流操作, 很耗费性能,所以JVM在进行类加载时是"懒加载"的, 迫不得已才加载.
我们把一定会触发类加载的场景,称之为类加载的时机,目前已经学过的有:
- 首次创建该类对象
- 启动该类中的main方法
局部变量与成员变量
学习目标:
掌握局部变量与成员变量的区别
局部变量和成员变量是Java程序开发时,最常见的两种变量类型,所以一定要搞清楚,它们之间的不同。
两者的区别
局部变量和成员变量的比较,我们从以下五个方面去比较:
- 在类中定义的位置不同
- 在内存中的位置不同
- 生命周期不同
- 有无默认初始化不同
- 作用范围
在类中定义的位置不同
- 局部变量定义在:方法、方法的形参或者代码块结构等局部位置。
- 成员变量 定义在:类体中、非局部位置的成员位置。
在内存中的位置不同
- 局部变量存储在栈上的栈帧中
- 成员变量存储在堆中的对象中
生命周期不同
局部变量随着方法的执行,而被创建,随着方法的执行结束就会被销毁。局部变量和方法"同生共死"。
成员变量在对象创建以后就存在了,对象被销毁回收内存自然就不存在了。
但实际上只要该对象栈上的引用被销毁,对象成为"垃圾",对象中的成员变量也就失去意义了。
有无默认初始化不同
- 局部变量没有默认初始化过程,必须手动初始化赋值。
- 成员变量,有默认的初始值。和数组一样,这里不再赘述。
作用范围
局部变量只在作用域的局部生效。
成员变量在整个类的成员方法中都可以使用,所以很多书籍也把成员变量称之为"全局变量"。
成员变量的赋值
限于目前的学习进度,其实你只知道两种给成员变量赋值的手段:
默认初始化,具有默认值。
显式赋值。即在类中定义成员变量时,直接了当的给出该成员变量的取值,就是显式赋值!比如:
成员变量的显式赋值演示
class Student{ // 类体 String name = "张三"; }
显式赋值和默认初始化赋值的顺序,任何时候都要记住,对象中的元素,默认初始化永远是第一步!在任何给成员变量赋值的手段执行前,默认初始化都已经执行了!
可以认为默认初始化是JVM在创建对象的内存结构时,它给出的!
this关键字
学习目标:
- 掌握this关键字的使用
引例
学习完对象与类后,做以下练习:
创建一个汽车类,有颜色(color)和速度(speed)两个属性,行为是可以跑(run)。
实现:
- 在run成员方法中访问速度和颜色两个属性,调用该方法查看结果。
- 在run成员方法的形参列表中,添加一个局部变量speed,不修改方法体,调用该方法查看结果。
思考:
- 两次方法调用的结果一样吗?为什么?
- 如果我想在2中得到1的访问结果,怎么办呢?
code
public class Demo {
public static void main(String[] args) {
// 创建Car对象
car car = new car();
// 调用成员方法
//car.run();
car.run(200);
}
}
class car{
// 定义成员变量
String color = "黑色";
double speed = 120.0;
//// 定义成员方法
//public void run(){
// System.out.println(color + "的车在高速公路上以" + speed + "公里每小时疾驰!");
//}
// 定义成员方法
public void run(double speed){
System.out.println(color + "的车在高速公路上以" + this.speed + "公里每小时疾驰!");
}
}
很明显,由于就近原则的影响:
这个时候,如果还想访问同名成员变量,普通的手段是做不到了,就需要this关键字来实现访问。
概念
this关键字(重点):
Java类中的每个成员方法的形参列表中都隐含了一个传参(隐式传参),传入的是当前对象,用this关键字指向!(为什么类中的所有成员方法都可以访问到成员变量---->就是因为this的存在)
所以:
this是一个引用,这个引用指向当前对象。
何为当前对象?
很多同学,在初学this时,会经常疑惑何为当前对象呢?
其实非常简单:
this指向当前对象,是类中成员方法的一个隐式传参。
成员方法总会需要一个对象,使用对象名点来调用,那么这个调用该成员方法的对象,就是当前对象!
Demo
Student stu = new Student();
stu.study();
stu.sleep();
验证当前对象就是调用方法的那个对象
package _04oop.com.cskaoyan._02this._02verfiy;
/
* @description:
* @author: 景天
* @date: 2022/11/8 9:55
/
public class Demo {
public static void main(String[] args) {
// 创建对象
Student student = new Student();
// 调用方法
student.printThis();
// 打印对象
System.out.println(student);
}
}
class Student{
// 定义成员方法
public void printThis(){
System.out.println(this);
}
}
this关键字的作用
既然this已经指向当前对象,是一个引用,那么它基本的用途就有:
- 在成员方法中,用this引用去访问类中成员变量和调用类中成员方法。由于this本身就是隐含的,所以一般情况下,可以省略this,直接访问类中成员。
- 特殊情况下,当成员方法中的局部变量和成员变量同名时,可以用 "this."访问 来表示访问同名成员变量,来和同名局部变量做区分。这种情况,this是不能省略的。
- 在成员方法中只要使用 "this."访问 一个变量,那么该变量一定是成员变量。在代码比较复杂的情况下,可以显著增加代码可读性, 可以使用this.成员变量对成员变量进行赋值--->set方法
注意事项
- this指向当前对象的隐含传参,必须是在普通成员方法中,加static的方法中,没有该this传参。(所以static方法不能直接访问类的成员,需要先创建对象才能访问。)
- 既然this指向当前对象,那么不同的this指向的对象必然不同。
构造方法
引例
创建一个教师类,有课程和年龄两个属性,行为是上课。
现在我们需要创建以下对象:
- 18岁的Java老师对象
- 28岁的C++老师对象
- 30岁的Python老师对象
- ...
按照之前我们的做法,需要先创建出对象,再进行成员变量的赋值。
Code
Teacher teacher = new Teacher();
teacher.course = "Java";
teacher.age = 20;
teacher.teach();
Teacher teacher2 = new Teacher();
teacher2.course = "C++";
teacher2.age = 21;
teacher2.teach();
Teacher teacher3 = new Teacher();
teacher3.course = "Python";
teacher3.age = 22;
teacher3.teach();
如果属性很多, 需要创建很多对象,就有点过于麻烦了。
对象的属性,能不能"出厂"的时候就设定好呢?想要在创建教师对象时,就直接指定这个对象的属性?
有这种需求时,就需要构造方法(constructor,也叫构造器)来完成了。
构造器语法
构造方法也是方法,但属于一种特殊的方法
[访问权限修饰符] 类名(形参列表){
// 构造方法体
}
说明:
权限修饰符先使用public
构造方法名必须跟类名相同(一模一样,包括大小写)
构造方法没有返回值, 也不需要写返回值
形参列表可以为空, 称为无参构造方法,非空为有参构造方法
构造方法体,和一般方法类似,可以写语句
构造器作用与使用
构造器的作用是用来给成员变量赋值的,完成对对象的初始化
说明:
- new关键字去创建对象的时候,JVM自动去调用构造方法,构造方法无法通过普通方法的调用方式调用。
- 构造器的作用不是创建对象,创建对象是JVM的事情。构造器只是告诉JVM在创建对象过程中,给成员变量赋什么值。
使用方式是:
new 类名(实参列表);
通过实参列表的不同,来判断调用哪个构造器。这实际也是方法重载的应用!
需求:
使用构造方法改进引例, 在创建教师对象时,就直接指定这个对象的属性(完成成员变量的赋值操作)
Code:
package _04oop.com.cskaoyan._03constructor._02defineuse;
/
* @description:
* @author: 景天
* @date: 2022/11/8 10:04
/
/*
使用构造方法改进引例, 在创建教师对象时,
就直接指定这个对象的属性(完成成员变量的赋值操作)
1. 18岁的Java老师对象
2. 28岁的C++老师对象
3. 30岁的Python老师对象
*/
public class Demo {
public static void main(String[] args) {
// 创建对象
Teacher t1 = new Teacher("Java", 18);
Teacher t2 = new Teacher("C++", 28);
Teacher t3 = new Teacher("Pyhond", 30);
// 调用方法
t1.teach();
t2.teach();
t3.teach();
}
}
// 定义教师类
class Teacher{
// 定义成员变量
String course;
int age;
/*
[访问权限修饰符] 类名(形参列表){
// 构造方法体
}
*/
// 无参构造方法
public Teacher(){
// 方法体
System.out.println("无参的构造方法执行了");
}
// 有参构造方法
public Teacher(String tCourse, int tAge){
// 方法体
System.out.println("有2个参数的构造方法执行了");
// 给成员变量赋值
course = tCourse;
age = tAge;
}
// 定义成员方法
public void teach(){
System.out.println(age + "的老师讲" + course);
}
//public void teach(String name){
// System.out.println(age + "的老师讲" + course);
//}
}
注意事项与使用细节
一个类中,是允许同时定义多个构造方法的,即构造方法重载,多个构造器的形参必须不同。
- 比如: 我们可以给Teacher类定义一个构造器, 用来创建对象的时候, 只指定课程, 不指定年龄
构造方法的名字必须和类名一模一样
构造器没有返回值
构造器是完成对象的初始化(给对象赋值), 并不是创建对象, 创建对象时, 系统根据实参列表自动调用该类的构造器
类中默认提供的无参构造方法,是在该类没有任何构造器的情况下才有的。但是如果类中有任一构造器(有参/无参),那么就没有默认无参存在了
在构造器中也会隐含this传参, 我们可利用this对成员变量进行赋值, 也可以使用快捷键, alt + insert快速生成构造器.
构造器中还可以用this表示调用其它构造器,语法:
this(实参列表);
表示调用类中的其它构造器,根据实参列表决定调用哪个构造器。
注意,在构造器中使用this(实参)表示调用类中其他构造器时,这行代码一定要处在构造器代码的第一行!
当然,既然必须在第一行,那么也只能用一次了。
构造器的赋值顺序
这里我们不妨总结一下,学完构造器后,三种给成员变量赋值的方式:
通过下面的例子分析程序执行流程 , 以上三种赋值方式的执行顺序
class Person{
String name;
int age = 20;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person() {
}
}
// 创建Person对象,成员变量的值分别是什么
Person p = new Person("张三", 30);
执行流程:
- 加载Person类信息, 只会加载一次
- 堆中分配空间
- 完成对象初始化
- 默认初始化
- 显示赋值
- 构造器赋值
- 堆中的对象的地址返回给p(对象的引用)
在各式各样给成员变量赋值的方式中,构造器是最后执行的!!!
对象创建流程
Debug练习 对于下列程序代码,请指出创建Student对象,程序每一步的执行流程:
构造器Debug模式练习
public class Demo{
public static void main(String[] args){
Student stu = new Student(18,"王冰冰");
}
}
class Student {
int age = 10;
String name = "张三";
double a = 100;
public Student(int age) {
System.out.println("Student age");
this.age = age;
}
public Student() {
}
public Student(int age, String name) {
this(age);
System.out.println("Student age,name");
this.name = name;
}
}
在这个案例中,我们发现Student双参构造器中,有使用this调用单参构造器,那么它们的执行顺序是:
- 类加载不要忘记了。
- 程序会先找到,Student的双参构造器,但是并不会执行,而是
- 先跳转执行this构造器,但是this构造器中的代码,也不会立刻执行(因为构造器赋值一定最后执行),而是
- 先从上到下执行成员变量的显示赋值,然后
- 跳回执行this构造器代码
- 最后执行双参构造器代码
小练习
对象与类
完成以下案例,思考一下怎么能够找到系统中类、对象究竟是哪些?
- 用面向对象的思想模拟LOL当中盖伦的战斗
- 用面向对象的思想模拟植物大战僵尸
如何从需求中找到对象和类呢?
对象描述的是程序世界中的个体/实体,所以一般都是名词。
抽取出全体对象的共性特征,也就是成员变量和方法,就可以定义一个类。
注意事项:
对象之间如果需要交互,可以考虑持有对方的引用作为成员变量。
日常开发中,不建议随便把很多类定义在同一个Java文件中。
正确情况下,建议一个Java文件中,仅定义一个public class和文件名保持一致。
当然,如果确有需求,也是可以定义非public class的。(少见但不是见不到)
一个Java文件下的所有类,属于同包关系。
LOL
public class Demo {
public static void main(String[] args) {
Legend galen = new Legend("Galen", "沉默打击", "勇气", "爱的魔力转圈圈", "大宝剑");
galen.fight();
}
}
// 英雄类
class Legend {
String name;
String skillQ;
String skillW;
String skillE;
String skillR;
/*
定义构造器,从代码规范上来说,构造器写在
成员变量的下面
成员方法的上面
而且都要写在一起(实际上所有构造方法重载的方法都应该写在一起)
写在一起的顺序: 可以按照参数个数的多少去排列,个数少的在上面,多的在下面
*/
public Legend() {
}
public Legend(String name, String skillQ, String skillW, String skillE, String skillR) {
this.name = name;
this.skillQ = skillQ;
this.skillW = skillW;
this.skillE = skillE;
this.skillR = skillR;
}
public void fight() {
System.out.println("名字为" + name + "的英雄,正在使用"+skillR+"技能战斗!");
}
}
植物大战僵尸
public class Plant {
String name;
int price;
int hp;
// 攻击力
int damage;
/*
在开发中为了方便两个对象之间的交互
往往会让某个对象持有另外一个对象的引用作为自身的成员变量
这样对象之间的交互会更加灵活,是常用的做法
*/
Zombie plantZ;
public Plant() {
}
public Plant(String name, int price, int hp, int damage) {
this.name = name;
this.price = price;
this.hp = hp;
this.damage = damage;
}
// 行为
public void attack() {
System.out.println(name + "正在攻击" + plantZ.name);
}
public void beAttacked() {
System.out.println(name + "正在被" + plantZ.name + "攻击,现在还剩下" + (hp - plantZ.damage));
}
}
public class Zombie {
String name;
int hp;
// 攻击力
int damage;
double speed;
Plant zombieP;
public Zombie() {
}
public Zombie(String name, int hp, int damage, double speed) {
this.name = name;
this.hp = hp;
this.damage = damage;
this.speed = speed;
}
// 行为
public void attack() {
System.out.println(name + "正在攻击" + zombieP.name);
}
public void beAttacked() {
System.out.println(name + "正在被" + zombieP.name + "攻击,现在还剩下" + (hp - zombieP.damage));
}
public void move() {
System.out.println(name + "正在以" + speed + "的速度,接近你的脑子~~");
}
public void eat() {
System.out.println(name + "吃掉了你的脑子~真香~");
}
}
值传递练习
学习对象与类后,再复习一下如果方法的参数是需要一个对象,值传递是怎样进行的。
练习: 定义一个学生类,该类具有一个int属性age 1.在测试类中写一个方法,交换两个Student对象的age属性 请问能够交换成功吗?原因是什么?
2.在测试类中写一个方法,交换两个Student对象的引用(地址) 请问能够交换成功吗?原因是什么?
结论:
- Java只有值传递
- Java当中的方法可以改变对象的状态(成员变量的取值)但是无法直接交换两个引用(无法改变局部变量)
成员变量赋值顺序练习
public class Demo {
public static void main(String[] args) {
Student s = new Student(18, "长风");
System.out.println(s.age);
System.out.println(s.name);
System.out.println(s.var);
System.out.println(s.cat.price);
}
}
class Student {
int age = 10;
String name = "张三";
double var = 20;
public Student(int age) {
System.out.println("Student age");
this.age = age;
}
public Student() {
}
public Student(int age, String name) {
this(age);
System.out.println("Student age,name");
this.name = name;
}
Cat cat = new Cat(1000);
Cat c2;
}
class Cat {
double price;
public Cat() {
}
public Cat(double price) {
System.out.println("Cat price");
this.price = price;
}
}
思考程序输出的顺序,搞清楚其中赋值的顺序。
1.Demo类加载,main方法执行
2.Student类加载,创建Student对象,调用2参构造
3.此时2参构造不会执行,会因为this(单参)调用单参构造,
4.此时单参构造不会执行,会先对对象中成员变量默认初始化,进行默认赋值
5.进行显式赋值
6.Cat cat = new Cat(1000);执行到这里,会触发Cat类加载
7.调用Cat单参构造方法
8.Cat单参构造方法先不执行,先默认赋值,没有显式赋值,执行构造方法进行赋值
9.Student类中的成员全部显式赋值成功--->进行构造器赋值
10.Student的单参构造器先执行,再执行两参构造器
11.Student对象创建成功并完成了赋值--->回到main方法继续执行
12.输出相应结果
static关键字
学习目标:
- 掌握静态成员变量的使用与特点
- 掌握静态成员方法的使用与特点
- 掌握静态与非静态的区别
引例
场景如下:
一场篮球比赛, 梦之队有5名顶级球员Kobe, James, Stephen...... 都是神射手擅长投3分,投篮必进
每进一球, 队伍分数+3, 比赛结束, 统计一下该队伍得分情况, 写程序模拟这个场景.
思路:
- main里面定义int count, 用来记录分数
- 每当有1个球员进球, 分数+3
Code:
package _04oop.com.cskaoyan._04static._01introuction;
/
* @description:
* @author: 景天
* @date: 2022/11/11 9:48
/
/*
一场篮球比赛, 梦之队有5名顶级球员Kobe, James, Stephen...... 都是神射手擅长投3分,投篮必进
每进一球, 队伍分数+3, 比赛结束, 统计一下该队伍得分情况, 写程序模拟这个场景.
思路:
- main里面定义int count, 用来记录分数
- 每当有1个球员进球, 分数+3
*/
public class Demo {
public static void main(String[] args) {
// 定义一个计数器
int count = 0;
Player p1 = new Player("Kobe");
p1.shot();
count += 3;
System.out.println("分数: " + count);
Player p2 = new Player("James");
p2.shot();
count += 3;
System.out.println("分数: " + count);
Player p3 = new Player("Stephen");
p3.shot();
count += 3;
System.out.println("分数: " + count);
}
}
// 定义一个Player类
class Player {
// 定义成员变量
String name;
public Player(String name) {
this.name = name;
}
// 定义成员方法
public void shot() {
System.out.println(name + "进球了!");
}
}
问题分析:
int count这个值是个局部变量, 独立于对象之外的, 使用起来不是很方便
引出来static关键字.
静态成员
根据static修饰的内容不同, 有以下分类
- 静态成员变量, static修饰成员变量 (有的资料里也称为类变量, 类属性指的都是静态成员变量)
- 静态成员方法, static修饰成员方法(有的资料里也称为类方法, 指的还是静态成员方法)
统称为类的静态成员
静态成员变量
基本语法
[访问权限修饰符] static 数据类型 变量名;
使用与特点:
- 和普通成员变量一样,都具有默认值(默认值和普通成员变量是一样的)
- 静态成员变量属于类的,完全不需要创建对象使用。
- 访问和使用静态成员变量不推荐使用,而应该使用!
- 静态成员变量的访问/赋值/使用都不依赖于对象, 而是依赖于类
使用案例:
设计一个int 类型的count值来表示球队得分, 每有一个球员得分, 那么count值+3, 要求count被所有对象所共享的即可. 使用静态成员变量修改之前的引例.
class Player{
String name;
// 使用static关键字修饰成员变量, 使其成为静态成员变量.
static int count;
}
Code
package _04oop.com.cskaoyan._04static._02static_field;
/
* @description:
* @author: 景天
* @date: 2022/11/11 9:58
/
/*
设计一个int 类型的count值来表示球队得分, 每有一个球员得分,
那么count值+3, 要求count被所有对象所共享的即可. 使用静态成员变量修改之前的引例.
*/
public class Demo {
public static void main(String[] args) {
// 静态成员变量可以通过类名.方式访问
// 推荐使用类名.方式访问
System.out.println("得分: " + Player.count);
// 创建Player对象
Player p1 = new Player("Kobe");
// 投篮
p1.shot();
// 更改分数 + 3
p1.count += 3;
System.out.println("得分: " + p1.count);
// 创建Player对象
Player p2 = new Player("James");
// 投篮
p2.shot();
// 更改分数 + 3
p2.count += 3;
System.out.println("得分: " + p2.count);
// 创建Player对象
Player p3 = new Player("Stephen");
// 投篮
p3.shot();
// 更改分数 + 3
p3.count += 3;
System.out.println("得分: " + p3.count);
}
}
// 定义一个Player类
class Player {
// 定义成员变量
String name;
// 定义一个静态的成员变量,表示球队得分
static int count;
public Player(String name) {
this.name = name;
}
// 定义成员方法
public void shot() {
System.out.println(name + "进球了!");
}
}
内存及原理解析:
静态成员的访问并不依赖于创建对象,可以直接通过类名访问,其原因在于:
某个类的某个静态成员变量只有一份,且被所有对象共享,属于类,无需创建对象使用。
注意事项:
局部变量,已经被方法限制了作用域,不能用static修饰它!
静态成员方法
基本语法
[访问权限修饰符] static 返回值类型 方法名(形参列表){
//方法体
}
使用与特点
- 无需创建对象就可以直接通过类名点直接调用。
- 同一个类中的static方法互相调用可以省略类名,直接用方法名调用。(这就是我们之前方法的调用)
注意事项:
一个类中,静态方法无法直接调用非静态的方法和属性,也不能使用this,super关键字(super后面会讲),静态的方法只能访问静态的
经典错误:Non-static field/method xxx cannot be referenced from a static context
原因:静态方法调用的时候,完全有可能没有对象,没有对象普通成员就无法访问。
普通成员方法当中,既可以访问静态成员的, 也可以访问非静态成员。普通成员方法访问任意的
访问静态成员变量的时候,使用类名.变量名的形式访问,以示区别,增加代码可读性
类加载时机
静态成员需要在类加载时期,完成准备,类加载结束就能够使用。
所以访问类的静态成员,一定会触发该类的类加载。
总结,到目前学习过的类加载时机:
- new直接创建该类的对象。(首次)
- 启动该类中的main方法。
- 访问该类的静态成员(方法和变量) (首次)
static VS 非static
当我们了解static成员的特点后,静态成员和非静态成员的区别就很明显
我们从以下四个角度比较(拿成员变量为例)
- 成员的所属
- 在内存中的位置
- 在内存中出现的时间
- 调用方式
其比较的结论如下:
- 所属不同
- 静态成员变量属于类,所以也称为为类变量
- (普通)成员变量属于对象,所以也称为对象变量(实例变量)
- 在内存中的位置不同
- 静态变量存储于方法区的静态域(堆上的这个类所对应的字节码文件对象,即Class对象中),被所有对象共享
- 成员变量存储于堆内存,每个对象独享自己的成员变量
- 在内存中出现时间不同
- 静态变量随着类的加载而加载,比成员变量出现的要早
- 成员变量随着对象的创建而存在
- 调用方式不同
- 静态变量可以通过类名调用,也可以通过对象调用(不推荐)
- 成员变量只能通过对象名调用,必须创建对象
使用场景
这里,根据static关键字的一些特点来明确它的使用场景,给大家以后使用static关键字做一下参考。
静态成员变量:
- 属于全体对象所共享而不是独属于某个对象的成员变量
所以当存在需要所有对象共享的变量时,应该使用static修饰的静态成员变量。
- 在整个类全局独一份的(因为类加载只有一次)
所以,如果希望某个变量在类的全局独一份时,应该使用static修饰的静态成员变量。
举例1 :
创建一个学生类,用来描述我们班全体同学
要求:
属性:姓名,性别,年龄,学号,学校信息
行为:吃饭,学习
我们简单思考可以知道, 无论你是张三或者李四, 学校信息这个属性实质上应该是被全体同学所共有的属性,而不是独属于某个对象的, 这种场景下就可以使用static 修饰学校信息
举例2:
创建一个学生类, 有2个属性: 姓名String name , 学号int id
统计外部创建Student类对象的个数 假设给Student类的对象自动编号
这个编号第一次创建对象是10001 随后每创建一个新对象就+1
package _04oop.com.cskaoyan._04static._05use_case; / * @description: * @author: 景天 * @date: 2022/11/11 15:04 / /* 利用了static成员变量类全局唯一 且被共享 统计外部创建Student类对象的个数 假设给Student类的对象自动编号 这个编号第一次创建对象是10001 随后每创建一个新对象就+1 */ public class Demo { public static void main(String[] args) { Student s1 = new Student(); System.out.println(s1.id); Student s2 = new Student(); System.out.println(s2.id); Student s3 = new Student(); System.out.println(s3.id); Student s4 = new Student(); System.out.println(s4.id); System.out.println("共计创建了" + Student.count + "个学生"); } } class Student{ // 姓名 String name; // 学号 int id; // 统计学生数量 static int count; // 初始学号 static int initNumber = 10001; public Student() { // 学生数量+1 count++; // 学号递增 this.id = initNumber++; } }
静态成员方法:
- 静态方法的最主要特点就是
所以如果希望一个方法能够更方便快捷的去调用,可以把它声明为static修饰的静态成员方法。
- 根据静态成员方法调用简单的特点,当一个类中全部都是静态成员方法时,
类中的所有方法的调用都可以使用类名点去完成,这就是Java当中的"工具类"。比较典型的有:数组工具类Arrays、集合工具类Collections、数学工具类Math等等。
static执行顺序练习
请说明程序的输出结果,并分析流程
public class Demo {
static Cat cat = new Cat();
Dog dog = new Dog();
Dog dog2;
static Dog dog3;
public static void main(String[] args) {
System.out.println("hello world!");
Demo d = new Demo();
}
public Demo() {
System.out.println("demo");
}
}
class Cat {
static Dog dog = new Dog();
public Cat() {
System.out.println("cat");
}
}
class Dog {
public Dog() {
System.out.println("dog");
}
}
你能得出什么结论?
1.静态成员变量的显式赋值,是在类加载过程中执行的。不管何种方式触发该类的类加载,这个过程都要执行。2.类加载可以连环触发,一个类可以最先开始类加载,但是不一定会最先完成类加载
3.无论是静态成员变量还是成员变量,只有声明且没有其它任何赋值方式赋值,那就只有默认值。
匿名对象
学习目标:
- 掌握匿名对象的概念
- 掌握匿名对象的用途与特点
什么是匿名对象
在Java当中,匿名对象指的就是没有名字的对象。
或者,说的更清楚一点,就是:
匿名对象的语法很简单,只需要在方法等位置中写下列语法:
new 类名();
以上语法就得到了一个匿名对象,从实质上看:
该对象没有栈上的引用指向,没有所谓的"对象名",是一个。
匿名对象的用途
匿名对象主要有两个用途(常见用途):
使用匿名对象作为方法的实参
当定义以下方法时,表示该方法需要传入一个对象(这个对象必须是"类名"的对象或者子类对象)
[修饰符列表] 返回值类型 方法名(类名 对象名){ //方法体 }
这个时候,常规的做法是创建对象,然后传入引用。但实际上这里可以直接传入匿名对象。
假如一个test方法需要传入一个Student对象,就可以这么写:
test(new Student());
使用匿名对象作为方法的返回值
当定义以下方法时,表示该方法需要传入一个对象(这个对象必须是"类名"的对象或者子类对象)
[修饰符列表] 类名 方法名(形参列表){ //方法体 return (匿名对象); }
匿名对象的优缺点
优点:
匿名对象在使用完毕后会立刻成为"垃圾"等待GC回收,从理论角度上来说,可以提升内存利用率。
但是,并不是一个对象更早成为"垃圾"就会更好回收,这个优点仅是理论上的。
缺点:
匿名对象由于没有引用指向,所以它是,用完后就无法再次使用了。
匿名对象使用场景
我们根据匿名对象的优缺点,很容易总结出匿名对象的使用场景:
- 需要一个对象去实现功能,并且该对象仅需使用一次即可,为了代码简洁,推荐使用匿名对象
- 但是,如果一个对象可能会被复用,像以下代码频繁创建匿名对象是得不偿失的
不要滥用匿名对象
test(new Student());
test(new Student());
test(new Student());
...
创建对象是需要耗时耗费内存空间的,不要为了一时的方便,频繁创建匿名对象。
总之:
如果某个对象,仅使用一次,使用匿名对象简洁方便。
但如果有多次使用某个类的对象的需求时,频繁使用匿名对象会导致频繁创建对象,降低代码性能,得不偿失!
代码块
学习目标
- 掌握代码块的几种分类
- 掌握代码块的作用与特点
- 掌握几种代码块的执行顺序
概述
之前,我们理解的代码块就是为了限制局部变量的一个大括号,今天来详细的学习一下代码块。
代码块的定义:由若干条Java语句组成,并且用一对大括号括起来的结构,叫做代码块。
代码块的分类,根据其位置和声明方式的不同,可以分为:
局部代码块
构造代码块
静态代码块
同步代码块
注:同步代码块,涉及多线程知识,后面多线程再学习,今天略过它。
构造代码块
语法定义
什么是构造代码块 ?
- 定义在类的成员位置,使用以下声明方式声明的代码块,称之为构造代码块。
//成员位置
{
// 局部位置
}
//成员位置
这个语法只有一个需要注意的地方:
作用
随着构造器的执行,用于在创建对象过程中,给成员变量赋值
这里总结给成员变量赋值的几种方式(创建对象过程中):
- 默认初始化,具有默认值
- 显式赋值
- 构造代码块
- 构造器
学习对象中成员变量的赋值,和赋值顺序要遵循的原则:
- :默认初始化,具有默认值,在对象结构存在于对象中,对象中的成员变量就已经具有了默认值。
我们程序员所有能干预的赋值方式,都是在默认初始化的基础上进行的。
- :构造器,构造器在整个对象的成员变量赋值过程中,处在最后的阶段,最后被执行。
明确以上两点后,我们现在只需要研究和的赋值顺序,
经过代码测试,我们发现:
- 这两个结构,谁写在代码书写顺序的上面,谁就先执行。
- 后执行结构的结构,自然会覆盖先执行结构的结果。
这样,类中显然会出现类似以下代码:
//构造代码块
{
a = 10;
}
int a = 1;
这种代码形式,按照从上到下的顺序来看的话,显然有些奇怪——成员变量还未定义,却进行了赋值。
原理
通过查看,我们发现编译后的代码中并不存在的结构,而是:
直接将成员变量的显式赋值和构造代码块中的代码智能地加入,类所有的构造器中的前几行:
所谓智能是为了保证:成员变量的显式赋值和构造代码块,按照代码的书写顺序从上到下执行!
于是,我们可以得出以下结论:
- 使用new对象的方式创建对象,不论使用哪个构造器,构造代码块都会随之执行。
- 构造器是每一次new对象都会执行一次,所以构造代码块也会随之执行一次。
- 构造代码块中的代码要放入构造器的首几行,
创建对象过程中的执行顺序
总结一下到目前为止,创建对象过程中可能碰到的结构的执行顺序:
new对象过程中,各种结构的执行顺序:
对象结构存在后就进行默认初始化,所有成员变量都具有默认值后,再开始其余赋值操作
找到new对象的那个构造器
- 如果它的首行显式地调用了另一个构造器this(实参)
(注:显式调用构造器目前指的是this调用自身构造器,其它场景这里先不考虑)
那么程序会先跳转到那个构造器,但是不会立刻执行,而是:
- 按照类中构造代码块和显式赋值的代码书写顺序,从上到下执行其中的代码,执行完毕后:
- 跳转回this语句要指示执行的构造器,执行其中的代码,然后:
- 跳转回new对象构造器,执行完毕后,创建对象结束。
- 如果它的首行没有显式调用另一个构造器
那么会先从上到下执行构造代码块和显式赋值代码,执行完毕后:
跳转回new对象构造器,执行完毕后,创建对象结束。
实际用途
构造代码块最大的特点就是其中的代码,最终会加入类的所有构造器中,所以依据这一点:
我们可以一样的,把所有构造器都需要执行的代码,放入构造代码块中。
其次,构造代码块毕竟是给成员变量赋值用的,所以:
如果需要很复杂的代码完成成员变量的赋值(如果只是给一个值,用显式赋值和构造器足矣)
比如需要一个算法,需要一定计算等等。在这些场景下,使用构造代码块赋值也是一个不错的选择。
练习
创建一个类Student,类中有多个构造器,请写代码统计外部创建Student对象的次数。
读程序题:
代码块练习题
public class Demo { public static void main(String[] args) { Cat c = new Cat(28, "紫色"); System.out.println(c.age); System.out.println(c.color); } } class Cat { { age = 18; System.out.println("age building block"); } int age = 10; String color = "黄色"; { color = "黑色"; System.out.println("color building block"); } public Cat() { } public Cat(int age) { System.out.println("age constructor"); this.age = age; } public Cat(int age, String color) { this(age); System.out.println("age color constructor"); this.color = color; } }
说出程序输出的顺序,并说明原因。
最后思考:构造代码块能不能用于给静态成员变量赋值?
静态代码块
语法定义
什么是静态代码块?
定义在类的成员位置,使用以下声明方式声明的代码块,称之为静态代码块
//成员位置
static{
// 局部位置
}
//成员位置
这个语法只有一个需要注意的地方:
作用
静态代码块在类中的作用
随着类加载的过程而执行,静态代码块可以看成是一个在类加载过程中,会自动调用的静态方法!用于给静态成员变量赋值!
这里还是想再强调一下:
想要一段语句,能够在类加载过程中自动被调用,需要使用静态代码块,而不是静态方法!!
这里总结一下给静态成员变量赋值的几种方式(类加载时期):
- 默认初始化,具有默认值
- 显式赋值
- 静态代码块
在这三个赋值方式中,默认初始化是永远第一步进行的,和的执行顺序:
- 按照代码的书写顺序去执行,谁写在代码顺序的上面,谁就先执行。
- 后执行结构的结果,会覆盖掉先执行结构的结果。
至于说到原理,静态代码块的执行,是JVM层面进行类加载的一种设计机制,是类加载的特殊设计机制保证的。
实际用途
数据库中加载JDBC驱动(最经典的),也可能是最常见的。
- (依赖于类加载只有一次的原理)
比如一些初类的始化工作,就可以放在静态代码块中完成。
最常见的就是类System的初始化,源码如下:
System类的初始化,依赖于本地方法的执行。
注意事项
一些细节问题(重要)
静态代码块可以近似看成一个,,所以不能在里面调用非静态。(没有对象)
包括this关键字,和后面学习的super关键字,都不能使用。
这意味虽然构造代码块可以给静态成员变量赋值,但静态代码块不能给成员变量赋值。
说白了,还是要搞清楚,谁先谁后执行的问题!
当需要使用复杂的代码给静态成员变量赋值时,可以使用静态代码块。
但如果仅仅是简单的赋值,直接显式赋值即可。
总得来说,静态代码块用得不多。
静态代码块也经常被用来测试类加载的顺序(重要)
一个类的静态代码块如果没有被执行,说明它没有被完全类加载。
补充类加载过程
首先,一个类从被加载到JVM内存中开始,到卸载出内存为止,一个类的生命周期包括:
一共是:
加载(Loading)
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
使用(Using)
卸载(Unloading)
注:其中,验证、准备和解析可以统称为。
一个类在JVM中的生命周期,共有七个阶段。
其中"加载 --> 连接 ---> 初始化"这三个步骤,即一个类的类加载过程。
这三步主要做:
- 主要是做通过类加载器(ClassLoader)将class字节码文件读取进JVM内存的操作, 并在内存中生成这个类对应的Class对象
在过程中:
验证,主要目的是为了确保class文件的字节流中包含的信息符合当前JVM的要求,不会影响JVM的安全。
准备,主要目的是进行静态成员变量的默认初始化,设置初始值。
这样,就保证了静态成员变量的默认初始化,永远最先进行。
解析,主要目的是将符号引用替换为直接引用。(不理解算了,目前不需要详细了解)
- 是类加载的最后一个步骤。主要目的是执行和static相关的内容,包括:
执行静态成员变量的显式赋值。
执行静态代码块。
以上,关于类加载各步骤,大致的作用,我们就了解了。
所以这里要重新认识一个概念:
类加载的时机,说得更准确一点,应该是。当然,想要初始化一个类,必然要先进行加载和连接。
练习
类加载过程练习一
public class Demo {
static {
System.out.println("Demo类开始初始化步骤了!");
}
static Cat cat = new Cat();
Dog dog = new Dog();
public static void main(String[] args) {
System.out.println("hello world!");
Demo d = new Demo();
}
public Demo() {
System.out.println("demo");
}
}
class Cat {
static {
System.out.println("Cat类开始初始化步骤了!");
}
static Dog dog = new Dog();
public Cat() {
System.out.println("cat");
}
}
class Dog {
static {
System.out.println("Dog类开始初始化步骤了!");
}
static Demo demo = new Demo();
public Dog() {
System.out.println("dog");
}
}
类加载练习二
public class TestStaticDemo {
public static void main(String[] args) {
staticMethod();
}
static TestStaticDemo ts = new TestStaticDemo();
static {
System.out.println("静态代码块");
}
{
System.out.println("构造代码块");
}
public TestStaticDemo() {
System.out.println("无参构造器");
System.out.println("a=" + a + ",b=" + b);
}
public static void staticMethod() {
System.out.println("静态成员方法");
}
int a = 666;
static int b = 777;
static TestStaticDemo ts2 = new TestStaticDemo();
}
代码块综合练习
public class ExerciseBlock {
static {
System.out.println("main方法静态代码块!");
}
{
System.out.println("main方法构造代码块!");
}
public static void main(String[] args) {
System.out.println("main方法开始执行!");
Star s = new Star(18,"马化腾");
System.out.println(Star.name);
System.out.println(s.age);
}
}
class Star{
{
age = 18;
Star.name = "杨超越";
System.out.println("我喜欢杨超越");
}
static String name = "王菲";
int age = 28;
static {
name = "杨幂";
System.out.println("我喜欢杨幂");
}
public Star(int age,String name) {
this(age);
System.out.println("age,name:构造器!");
Star.name = name;
Star.name = "刘亦菲";
}
public Star(int age) {
System.out.println("age:构造器!");
this.age = age;
}
public Star() {
}
}
通过上述程序,不难得出,代码块执行的顺序:
- 静态代码块
- 构造代码块
- 构造器
导包
学习目标:
- 会使用import
- 知道什么是全类名
package关键字
package这个关键字我们并不陌生,它写在Java源文件的第一行,用于声明整个Java文件下的所有类的所属包。
语法:
package + 包名
当然,一个Java源文件当中,只能有一个public修饰的类。其余非public修饰的类和public类都属于同包的关系。
注意:
package关键字的使用很简单,但是有几个需要注意的地方::
- 包名在书写时,如果存在多级包名,需要使用用.隔开
- package声明必须处在一个Java文件有效代码的第一行,否则会报错
- 注释不算有效代码,将package声明放在注释下面也是可以的
- 建议将package声明永远放在Java源文件真正意义上的第一行
- 多数情况下,我们使用idea新建Java文件是无需关心package声明的,因为idea会自动生成
- 但是当你从网上或者其它途径弄到的一些代码,可能会出现包名错乱的情况
- 这时建议直接删除package声明,然后"Alt + 回车"类名报错的地方即可
全限定类名
什么是全限定类名?
- 可以唯一的、准确的定位到一个类的,由包名加上类名组成的字符串,就是全限定类名。
- ,直接输出一个对象的引用,会打印该类的全限定类名
import关键字
引例
请按照以下步骤创建需要的类:
- 在包名为one的包中创建一个public class Student和测试类public class Demo
- 在包名为another的包中创建一个public class Student和public class Teacher
- 在同名的Student类中给出同名的方法test,并给出不同的实现
- 创建完毕后,开始进行测试工作
请完成下面测试:
1.在Demo类中直接创建Student对象,然后调用test方法,请问输出的结果是什么? 2.如果想要test方法调用得出anotherpackage包中Student类的结果,需要怎么做?
显然:
在Test类中直接创建Student类对象时,test方法的调用结果是onepackage下Student类的方法执行结果。
而如果想要test方法的调用结果体现为anotherpackage包下的Student类,就需要使用import关键字进行导包操作。
作用
从上述案例中,我们可以总结一下编译器在查找并决定使用某个class时,它的搜索机制:
在同包下时,类名是绝对唯一的,有就有,没有就没有,不存在选择的问题。
- 这时,编译器是可以直接通过一个类名去查找到一个类的,不需要额外操作,不需要导包。
- 这可以看成一种"就近原则",同包已经存在这个类了,自然不需要去外面找。
当同包下没有这个类,必须在不同包下寻找时,多个不同包中完全可能存在同名类。
- 这时,编译器肯定是不可能直接通过类名去查找一个类了,需要程序员手动导包。
- 手动导包的目的是明确告诉编译器应该使用哪个包下的类。
使用import关键字手动导包的语法是:
import 全限定类名;
注意事项:
import导包语句应该放在package声明之后,有效代码之前。
正常情况下,我们使用某个类时,IDEA会自动导包,不用太关心这条语句的位置。
像String、System、Math等常用类很明显不是同包下的类,但我们并没有进行导包操作。
- 这是因为在Java当中的,是Java的核心类包,具有隐式的导包。
- 注意"java.lang包"下的所有类,是隐式的导入了每个Java类,而不是没有导入!
实际上完全可以不导包去使用不同包下类,这时要明确指出这个类的所属包,也就是要使用全限定类名。
但是一般情况下,全限定类名都很长,导包仍然是更好的手段。
但是导包总不是都好用,在极少的情况下:
比如,在Demo类中,想要同时访问两个包onepackage、anotherpackage下的两个Student类。
咋办?
很简单,其中一个Student类使用全限定类名,另一个导包或者就近原则直接使用就可以了。
(当然条件允许的情况下,干脆把其中一个Studnet改名会更好。)
智能导入
import关键字存在智能导包的形式,会智能的导入需要的某个类
它的语法是这样的:
import + 包名.*;
其中具有通配的含义,表示该包的类都会导入。
智能导包是一种十分高效且方便的导包方式,
所谓根据需求,即是:不导入对应包下的类就会报错,不导入不行。反之如果同包下有同名类则不会导入!
例如:同包中已存在Student类,再想通过智能导包导入别的包下的Student类是无法完成的。
访问权限修饰符
概述
程序的开发,不可能是一个人完成的,而是需要分工协作的。那么怎么保证"我想要被别人访问的地方,允许访问。不想让别人访问的地方,禁止访问"这种合情合理的需求呢?
访问权限控制符:
在Java语言中,一切事物(类和类的所有成员)都具有(或显示定义或隐式定义的)访问权限,而这种语言层面的访问权限控制,是由访问权限修饰符实现的。
访问级别
Java的访问权限的级别,是依赖包(package)来实现的。
Java的访问权限级别共分为以下四个级别,访问权限从严格到宽松顺序为:
- private: 只能够在同一类中能够访问,私有的,外面谁都不能用。
- 缺省(默认): 同一包中的子类或者其它类能够访问,同包中都可以使用。
- protected: 不同包的子类能够访问。(这个访问级别继承再学习)
- public: 不同包的其他类能够访问。
public | protected | (缺省) | private | |
---|---|---|---|---|
同一类中 | ||||
同一包其他类 | ||||
不同包子类 | ||||
不同包其他类 |
分类
类的访问权限修饰符
对于类的访问权限控制,非常简单,只有两种:
- public:对其他任意类都可见。
- (缺省的)不写任何关键字,表示对同包中的其它类可见。
思考:为什么class没有私有或者protected之类的权限?
这是因为,单独定义的class是一个独立的概念,它没有对谁私有,受谁保护的概念。
所以一般的class的访问权限修饰符只有两种,但是类是可以嵌套定义的。
内部类就有了私有之类的概念,就有了更多的访问权限修饰符。(这个后面会详细学习,先了解一下)
类中结构的访问权限修饰符
对于的访问权限,访问权限修饰符总位于它们定义的开头位置,可以使用的修饰符有4种:
- public:任意类均能访问,实际就是没有限制访问权限。
- protected:同包中的其他类都可以访问,不同包下必须是子类才能够访问。
- (缺省的)什么关键字都不写,表示同包中的其他类都可以访问。
- private:仅对自身类中的其他成员可见。
注意事项(小细节):
protected权限,涉及继承的概念,我们留到继承的章节中学习,这里直接跳过不学习!
按照以往的经验,有些同学对私有private的权限有疑问,这里还是要强调一下:
比如参考下列代码:
private小细节
// 类Student的类体中 private int age; public void test(Student s){ System.out.println(s.age); } // 类Student的类体中
方法传入了一个Student对象,虽然这里是外部调用方法传入的对象,但仍然是Student类的对象。
处在Student类中,访问Student类的私有成员,不管这个Student类对象哪里来的,都是完全没问题的!
局部变量,已经被作用域限制死了作用范围,访问权限对它而言毫无意义。
局部变量不能使用访问权限修饰符修饰!
作用
告诉代码的使用者,哪些地方不要触碰,哪些地方应该使用。起到警告、约束和指导代码使用者的功能。
举例来说:
专门提供给外界使用的,推荐使用的,用public。告诉别人:这里是你需要关注、了解和使用的地方。
不应该触碰的地方,用私有private修饰,告诉别人:这里你不需要你看,也不需要你管。
具体的案例是:
工具类,既然所有的方法,都可以直接用去调用,不需要创建对象!
既然不需要对象,那就干脆不要创建对象,直接把这个功能去掉!
类中需要提供给外界使用的方法的实现过程中,经常需要一些方法。
这些方法不需要提供给外界使用,仅作为内部实现功能使用。
好处是显而易见的:把和给区分开来了:
所以,我们就可以总结出,我们在实际开发中,使用的原则:
- 尽量私有化,方便自己修改代码,隐藏实现细节。
- 如果不能私有化,那也应该尽量少的给予访问权限,先从默认权限开始考虑。
- 只有确定这个结构,一定需要在外部(尤其是不同包下)被访问时,才考虑给public,尽量少给public。
权限这个东西,要吝啬,不要"大方"!