前言:
本博客将带大家了解JAVA中的类和对象的相关知识。
目录
大体梳理
希望上图可以帮助大家更好地理解类和对象,不过博主也只是个新手,如果发现错误,欢迎大家多多指正。
面向对象的初步认识
- 面向对象是一种解决问题的思想,主要依靠对象之间的交互完成一件事情。
- Java是一门纯面向对象的语言(Object Oriented Program,简称OOP),在面向对象里,一切皆对象。
以去出差为例:
面向过程:有一种step by step的感觉,用这种方法写代码,扩展或者维护起来会比较困难
要出差-->规划路线-->订机票-->订酒店-->收拾行李
面向对象:只需要告诉一个对象你所要做的事情,然后那个对象会帮你做好
要出差-->告诉秘书(秘书按照上面的面向过程的流程来做)
注意:面向对象和面向过程并不是一门语言,而是解决问题的办法,没有好坏之分,都有各自专门的应用场景。
类的定义和使用
类的定义:类是用来对一个实体(对象)进行描述的,主要描述该实体(对象)具有哪些属性(外观,尺寸之类的),哪些功能。采用Java预见将狗在计算机中定义完成,经过Java编译之后就形成了.class文件,在JVM的基础上计算机就可以识别了。
以小狗为例:
小狗具有名字、毛发颜色、汪汪叫和睡觉等属性(自然,这里只是稍加举例,没有全部列举狗狗的全部属性)
于是乎,我们可以写一个这样的类:(认真看注释)
//创建类:
class Dog{//给类定义时需要用到class关键字,类名采用大驼峰的定义方法
//类中包含的内容称为类的成员
//属性是用来描述类的,称为类的成员属性(类的成员变量)
public String name;//狗狗的名字
public String color;//狗狗毛发的颜色
//方法主要说明类具有哪些功能,称为类的成员方法
public void wangWang(){
System.out.println("汪汪叫!");
}
public void sleep(){
System.out.println("睡觉!");
}
}
此段代码中:
class--->定义类的关键字
Dog--->类的名字
{}--->里面是类的主体
注意:
- 类名应该采用大驼峰的定义方法(即每个单词的首字母都要大写)
- 辨析局部变量和成员变量:局部变量定义在方法的内部,成员变量定义在类的里面,方法的外面
- 一般一个文件只定义一个类
- main方法所在的类一般要使用public修饰(Eclipse默认会在public修饰的类中找main方法)
- public修饰的类必须要和文件名相同
- 不要轻易去修改public修饰的类的名称,如果要修改请按照下图所示的方法修改
类的实例化
定义了一个类,就相当于在计算机中定义了一种新的类型(这里指的类型是可以类比于int、double类型差不多的那种)。下面的代码将帮你更好地理解这句话:
//定义一个person类:
class Person{
public String name;
public int age;
public void eat(){
System.out.println("吃饭!");
}
public void sleep(){
System.out.println("睡觉!");
}
}
public class Main {
public static void main(String[] args) {
//实例化一个person对象:
Person person1 = new Person();//通过new来实例化一个对象
person1.age = 10;//.起的是引用作用
person1.name = "zhangsan";
//上面这两个语句其实可以类比int和String类型
/*int age = 10;
String name = "zhangsan";*/
System.out.println(person1.name+" "+person1.age);
person1.eat();
person1.sleep();
}
}
效果:
何为类的实例化:
用类创建对象的过程称为类的实例化,在Java中通过new关键字配合类名实例化对象。
注意:
- 使用 . 来访问对象中的属性和方法(也就是所谓的引用)
- 同一个类可以创建多个对象(类就类似于一张房子的设计图,每实例化一个对象就相当于照着这张图纸建一座房子,实例出来的房子结构一样,用的材料不一样)
//定义一个person类: class Person{ public String name; public int age; public void eat(){ System.out.println("吃饭!"); } public void sleep(){ System.out.println("睡觉!"); } } public class Main { public static void main1(String[] args) { //实例化一个person对象: Person person1 = new Person();//通过new来实例化 person1.age = 10;//.起的是引用作用 person1.name = "zhangsan"; System.out.println(person1.name+" "+person1.age); person1.eat(); person1.sleep(); System.out.println("==========");//分割线 //一个类可以实例化多个对象 Person person2 = new Person(); person2.name = "lisi"; person2.age = 30; System.out.println(person2.name+" "+person2.age); person2.eat(); person2.sleep(); } }
- 当一个引用赋值为null的时候,意味着这个引用不指向任何对象
//定义一个person类: class Person{ public String name; public int age; public void eat(){ System.out.println("吃饭!"); } public void sleep(){ System.out.println("睡觉!"); } } public class Main { public static void main1(String[] args) { //一个引用赋值为null的时候,意味着这个引用不指向任何对象: Person person1 = null;//这意味着person1不指向任何对象 /*person1.name = "zhangsan"; person1.age = 10;*/ //因为person1不指向任何对象,所以它不能访问name和age,会出现空指针异常 } }
- 引用不能指向引用
//定义一个person类: class Person{ public String name; public int age; public void eat(){ System.out.println("吃饭!"); } public void sleep(){ System.out.println("睡觉!"); } } public class Main { public static void main1(String[] args) { Person person2 = new Person(); person2.name = "lisi"; person2.age = 10; Person person3 = new Person(); person3.name = "wangwu"; person3.age = 10; //引用不能指向引用: person3 = person2; //这表示person3这个引用指向了person2这个引用指向的对象 } }
- 一个引用不能同时指向多个对象
//定义一个person类: class Person{ public String name; public int age; public void eat(){ System.out.println("吃饭!"); } public void sleep(){ System.out.println("睡觉!"); } } public class Main { public static void main1(String[] args) { //一个引用不能同时指向多个对象: Person person4 = new Person(); person4 = new Person(); person4 = new Person(); person4 = new Person(); person4 = new Person(); //即使它new这么多的Person,它还是只指向最后一个new的Person } }
this引用
为什么要有this引用
public class DateUtil {
public int year;
public int month;
public int day;
public void setDate(int year,int month,int day){
year = year;
month = month;
day = day;
}
public void show(){
System.out.println("年:"+this.year+" 月:"+this.month+" 日:"+this.day);
}
public static void main(String[] args) {
DateUtil dateUtil1 = new DateUtil();
dateUtil1.setDate(2022,11,27);
dateUtil1.show();
DateUtil dateUtil2 = new DateUtil();
dateUtil2.setDate(2022,11,28);
dateUtil2.show();
DateUtil dateUtil3 = new DateUtil();
dateUtil3.setDate(2022,11,29);
dateUtil3.show();
}
}
上面代码中,形参名和成员变量名不小心一样了:
那么:setDate中的赋值情况到底是怎么样的?是成员变量给成员变量赋值?参数给参数赋值?成员变量给参数赋值?还是参数给成员变量赋值?
根据调试我们发现,在setDate方法里面的赋值情况就是参数给参数赋值(就近访问原则,优先访问近的,在这里就是局部变量较之成员变量更近,所以优先局部变量)因为我们能很直观地看到成员变量并没有发生任何改变,也没有传进去setDate方法里面,这样下来,其实setDate方法就和没有是一样的,而我们本意是想要通过setDate方法来修改成员变量year,month和day。所以这时就需要借助this引用来帮助我们实现了。
什么是this引用
this引用指向当前对象(通俗点来说就是正在调用方法的那个对象,想知道this是谁,就看谁调用了这个方法),在成员方法中所有成员变量的操作,都是通过this引用去访问的。而且是编译器自动完成(不需要我们自己手动实现的那种)。
//每个成员方法的第一个参数默认是this(但是一般不会写)
//this是“成员方法”第一个隐藏的参数,编译器会自动传递,
//在成员方法执行时,编译器会负责将调用成员方法对象的引用传递给该成员方法,this负责来接收
public void setDate(DateUtil this, int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
this引用的用法:
1.访问成员变量
class DateUtil{
public int year;
public int month;
public int day;
public void setDate(int y, int m,int d){
this.year = y;
this.month = m;
this.day = d;
}
//如果不加this引用,可能就会出现只是把局部变量初始化了,没有给对象对应的year,month,day赋值,因为局部变量有优先使用
public void show(){
System.out.println("年:"+this.year+" 月:"+this.month+"日:"+this.day);//要习惯使用this,最好习惯加上this
}
}
public class KnowledgeOne {
public static void main(String[] args) {
//用法1:访问成员变量
DateUtil dateUtil = new DateUtil();//实例化对象,此时会调用对象的构造方法
dateUtil.setDate(2022,11,7);
dateUtil.show();//年:2022 月:11 日:07
DateUtil dateUtil2 = new DateUtil();
dateUtil2.setDate(2022,11,4);
dateUtil2.show();
DateUtil dateUtil3 = new DateUtil();
dateUtil3.setDate(2022,11,2);
dateUtil3.show();
}
}
2.访问构造方法
public class DateUtil {
public int year;
public int month;
public int day;
//带参数的构造方法:
public DateUtil(int year,int month, int day){
this.year = year;
this.month = month;
this.day = day;
System.out.println("调用了带有三个参数的构造方法!");
}
public void setDate(int y,int m,int d){
this.year = y;
this.month = m;
this.day = d;
}
public void show(){
System.out.println("年:"+this.year+" 月:"+this.month+" 日:"+this.day);
}
public static void main(String[] args) {
//有了带参数的构造方法以后,就可以直接在实例化对象的时候对对象进行赋值
DateUtil dateUtil1 = new DateUtil(2022,11,27);
dateUtil1.show();//年:2022 月:11 日:27
DateUtil dateUtil2 = new DateUtil(2022,11,28);
dateUtil2.show();//年:2022 月:11 日:28
DateUtil dateUtil3 = new DateUtil(2022,11,29);
dateUtil3.show();//年:2022 月:11 日:29
}
}
何为构造方法
构造方法(也称构造器)是一个特殊的成员方法,名字必须与类名相同,在创建对象时,由编译器自动调用,并且在整个对象的生命周期内只调用一次(通俗点说就是在new对象的同时调用构造方法)。注意,构造方法的作用就是对对象中的成员进行初始化,并不负责给对象开辟空间。
构造方法的特点
- 名字必须与类名相同
- 没有返回值类型,设置成void也不行
- 创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次
- 构造方法可以重载
- 如果用户没有显式定义,编译器会生成一份默认的构造方法,生成的默认构造方法一定是无参的;一旦用户定义,编译器则不再生成
- 构造方法中,可以通过this调用其他构造方法简化代码,但是 this()必须是构造方法中的第一条语句,而且也不能成环(见下面代码);
public class DateUtil { public int year; public int month; public int day; //不带参数的构造方法: public DateUtil(){ this(1999,9,9);//这个this引用会调用当前对象的其他构造方法(只能在构造方法里这么使用) //this去调用构造方法的时候必须放在第一行 System.out.println("不带参数的构造方法!"); } //带参数的构造方法: public DateUtil(int year,int month, int day){ this.year = year; this.month = month; this.day = day; System.out.println("调用了带有三个参数的构造方法!"); } //上面的this引用不能这么写:死循环 *//* //不带参数的构造方法: public DateUtil(){ this(1999,9,9); System.out.println("不带参数的构造方法!"); } //带参数的构造方法: public DateUtil(int year,int month, int day){ this();//上面的两个构造方法不能这么写,this引用会成环,出不来 this.year = year; this.month = month; this.day = day; System.out.println("调用了带有三个参数的构造方法!"); }*//* public void setDate(int y,int m,int d){ this.year = y; this.month = m; this.day = d; } public void show(){ System.out.println("年:"+this.year+" 月:"+this.month+" 日:"+this.day); } public static void main(String[] args) { //有了带参数的构造方法以后,就可以直接在实例化对象的时候对对象进行赋值 DateUtil dateUtil1 = new DateUtil(); dateUtil1.show(); //从输出也可以看出这个代码的运行情况是如何 } }
- 绝大多数情况下使用public来修饰,特殊场景下会被private修饰。
3.访问成员方法
public class DateUtil {
public int year;
public int month;
public int day;
public void setDate(int y,int m,int d){
this.year = y;
this.month = m;
this.day = d;
this.show();//访问成员方法
}
public void show(){
System.out.println("年:"+this.year+" 月:"+this.month+" 日:"+this.day);
}
public static void main(String[] args) {
DateUtil dateUtil1 = new DateUtil();
dateUtil1.setDate(2022,11,27);//年:2022 月:11 日:27
}
}
this引用的特性:
- this引用的类型:对应类类型引用,即哪个对象调用就是哪个对象的引用类型
- this只能在“方法成员”中使用
- 在“方法成员”中,this只能引用当前对象,不能再引用其他对象
- this是“成员方法”第一个隐藏的参数,编译器会自动传递,在成员方法执行时,编译器会负责将调用成员方法的对象的引用传递给该成员方法,this负责接收。
对象的构造及初始化
我们都知道,在Java方法内部定义一个局部变量时,必须要初始化,否则就会编译错误。
如图:
但对于成员变量,没有初始化,它好像也不会报错,这是为什么呢?同时,我们又该如何为成员变量赋值呢?以下将向大家介绍三种初始化的方法,同时也会穿插第一个问题的答案。
构造方法
经过上面对于构造方法的介绍,相信大家非常容易就理解了为什么构造方法可以帮助初始化成员变量了(在实例化对象的时候就可以对成员变量进行初始化),这里就不作过多介绍了。
默认初始化
之前我们一直都好奇为什么成员变量不用初始化,而局部变量必须要初始化,现在,让我们来揭开谜底。
先上一段代码:
class Date{
public int year;
public int month;
public int day;
public Date(int year, int month, int day){
this.year = year;
this.month = month;
this.day = day;
System.out.println("Date(int,int ,int)方法被调用了");
}
public void printDate(){
System.out.println(year+"-"+month+"-"+day);
}
}
public class Main {
public static void main(String[] args) {
Date date = new Date(2022,11,12);
date.printDate();
}
}
要解开这个谜题,我们必须要了解一个事情,
图片中框起来的语句到底在JVM层面起了哪些作用:
- 检测对象对应的类是否加载了,如果没有加载则加载
- 为对象分配内存空间
- 处理并发安全问题。比如:多个线程同时申请对象,JVM要保证对象分配的空间不冲突
- 初始化所分配的空间(即:对象空间被申请好之后,对象包含的成员已经设置好了初始值。是的,你没有看错,它已经在这个过程初始化好了)
- 设置对象头信息
- 调用构造方法,给对象各个成员赋值。
上面第四小点,我们已经了解到引用的同时,成员变量会被初始化,那么它到底会把成员变量初始化什么呢?
就地初始化
在声明变量的时候直接给出了初始值
但是下图的这种做法是错误的:
显然,编译器在报错。
封装
面向对象程序有三大特征:封装、继承、多态。
封装的概念
将数据和操作数据进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互。所谓封装,简单来说就是套壳屏蔽细节。
Java中主要通过类和访问权限来实现封装:类可以将数据以及封装数据的方法结合在一起,更符合人类对事物的认知;而访问权限用来控制方法或者字段能否直接在类外使用。
访问限定符
Java中一共提供了四种访问限定符:private、默认权限(default,它不是关键字,什么都不加,就是默认权限,只能在同一个包里面使用)、protected、public
下图为各个限定符的访问权限(涂红表示可以访问)
注意:
- 访问限定符不仅可以限定类中的成员的可见性,还可以控制类的可见性。
- 一般情况下,成员变量设置为private,成员变量设置为public。
包
- 何为包?为了更好地管理类,把多个类收集在一起成为一组,称为软件包。
- 包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式。
- 包还有一个重要的作用:在同一个工程中允许存在相同名称的类,只要处在不同的包中即可。
导入类中的包
- 可以直接在代码中导入:
public class KnowledgeOne { public static void main(String[] args) { java.util.Date date = new java.util.Date(); //得到一个毫秒级别的时间戳 System.out.println(date.getTime()); } }
- 通过import语句导入:
import java.util.Date; public class KnowledgeOne { public static void main(String[] args) { Date date = new Date(); //得到一个毫秒级别的时间戳 System.out.println(date.getTime()); } }
- 也可以通过*:
import java.util.*;//这种导入是要哪个就导入哪个(随用随取),不是一股脑全导入进来。 public class KnowledgeOne { public static void main(String[] args) { Date date = new Date(); //得到一个毫秒级别的时间戳 System.out.println(date.getTime()); } }
但是更建议使用显式的指定要导入的类名。否则容易出现冲突的情况
import java.util.*; import java.sql.*; public class KnowledgeOne { public static void main(String[] args) { //在util和sql中都存在一个名为Date的类,此时就会出现歧义,编译错误 Date date = new Date(); //得到一个毫秒级别的时间戳 System.out.println(date.getTime()); } }
效果:
在上面这种情况需要使用完整的类
- 可以通过import static导入包中静态的方法和字段:
import static java.lang.Math.*;//把所有的都拿进来了 public class KnowledgeOne { public static void main(String[] args) { double x= 30; double y = 40; //静态导入的方法写起来更方便一些 //double result = Math.sqrt(Math.pow(x,2) + Math.pow(y,2)); double result = sqrt(pow(x,2) + pow(y,2)); System.out.println(result); }
- 常见的包:
java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入
java.lang.reflect:Java反射编程包
java.sql:进行数据库开发的支持包
java.net:进行网络编程开发的包
java.util:是Java提供的工具程序包。(集合类等)非常重要
java.io:I/O编程开发包
自定义包
基本规则:
- 在文件的最上方加一个package语句,指定改代码在哪个包中
- 包名需尽量指定成唯一的名字,通常会用公司的域名的倒置形式,所有的英文字母都要小写(如:com.janx.www--->此域名完全是随手一取).
- 包名要和代码路径相匹配。例如创建com.janx.www的包,那么会存在一个对应的路径com/janx/www来存储代码。
- 如果一个类没有package语句,则该类被放到一个默认包中
操作步骤:
1.在IDEA中新建一个包:
2.成功创建:
3.在包中创建类:
4.创建成功以后,就会发现在新创建的Test.java文件的最上方出现了一个package语句:
包的访问权限控制举例:
package com.janx.www1;
public class Computer{
private String cpu;//cpu
private String memory;//内存
public String screen;//屏幕
String brand;//品牌
public Computer(String brand, String cpu, String memory, String screen){
this.brand = brand;
this.cpu = cpu;
this.memory = memory;
this.screen = screen;
}
public void Boot(){
System.out.println("开机~~~");
}
public void PowOff(){
System.out.println("关机~~~");
}
public void SurfInternet(){
System.out.println("上网~~~");
}
}
package com.janx.www2;
import com.janx.www1.Computer;//导入www1包里面的Computer类
public class Test2 {
public static void main(String[] args) {
Computer p = new Computer("HW","i7","8G","13*14");
System.out.println(p.screen);
/*System.out.println(p.cpu);//报错:cpu是私有的,不允许被其他类访问
System.out.println(p.brand);//报错:brand是default,不允许被其他包中的类访问
*/
//如果去掉Computer类之前的pubilc修饰符,代码也会编译失败。
}
}
static成员
class Student {
public String name;//姓名
public int age;//年龄
public int score;//分数
public Student(String name, int age, int score){
this.name = name;
this.age = age;
this. score = score;
}
}
public class Main {
public static void main(String[] args) {
Student student1 = new Student("zhangsan",12,78);
Student student2 = new Student("l;isi",14,89);
Student student3 = new Student("wangwu",13,78);
}
}
在上述代码中实例化了三个学生,每个学生都具有独一无二的名字、年龄和分数。试想,如果这三个学生恰好是同班同学,在同一个教室上课,我们是否能在类里面再加一个成员变量来保存这个不变的“教室”呢?答案是不行的。要想解决这个问题,静态成员是一个不错的选择。
那么,何为静态成员?
在Java中,被static修饰的成员称之为静态成员,也可以称为类成员。其不属于某个具体的对象,是所有对象所共享的。
static成员的特点
- 不属于某个具体的对象,是类的属性,所有对象共享,不储存在某个对象的空间中。
- 既可以通过对象访问,也可以通过类访问,但一般更推荐类访问。
- 类变量储存在方法区。
- 生命周期伴随类的一生(即,随类的加载而创建,随类的卸载而销毁)。
static修饰的成员方法
在Java中,被static修饰的成员方法称为静态成员方法,是类的方法,不是某个对象所特有的。静态成员方法一般是通过静态方法来访问的。
特性:
- 不属于某个对象,是类方法。
- 可以通过对象调用,也可以通过类名.静态方法名的方式调用。同样,这里推荐通过类名调用。
- 不能在静态方法中访问任何非静态成员变量。
- 静态方法中不能调用任何非静态方法,因为非静态方法有参数this,在静态方法中调用的时候无法传递this引用。
- 静态方法无法重写,不能用来实现多态
static成员变量初始化
静态成员变量一般不会放在构造方法中来初始化,构造方法中初始化的是与对象相关的实例属性。
静态成员变量的初始化方法有:就地初始化、通过get和set方法来初始化和静态代码块初始化。
就地初始化
非常简单,在定义的时候直接就给初始化了。
静态代码块初始化
class Student{
public String name;
public int age;
public static String classroom;
//构造方法:
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void doClass(){
System.out.println(name+"上课!");
}
static {//静态代码块
classroom = "五教112";//初始化
System.out.println("静态成员方法!");
}
}
代码块
代码块的概念
使用{}定义的一段代码成为代码块。
代码块的分类
根据代码块定义的位置以及关键字,可以分为以下四种:
普通代码块
构造代码块
静态代码块
同步代码块
普通代码块
定义在方法中的代码块
构造代码块
定义在类中的代码块(不加修饰符)。也叫实例代码块。构造代码块一般用于初始化实例成员变量。
class Student{
public String name;
public int age;
//构造方法:
public Student(String name, int age) {
this.name = name;
this.age = age;
}
{//构造代码块:
this.name = "小民";
this.age = 20;
System.out.println("构造代码块!");
}
public void doClass(){
System.out.println(name+"上课!");
}
}
public class KnowledgeTwo {
public static void main(String[] args) {
Student student1 = new Student("张三",10);
student1.doClass();
}
}
效果:
从运行结果(从代码调试更加能证明)来看,我们可以知晓,代码的执行顺序是:构造代码块-->构造方法。
静态代码块
使用static定义的代码块称为静态代码块。一般用于初始化静态成员变量。
class Student{
public String name;
public int age;
public static String classroom;
//构造方法:
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void doClass(){
System.out.println(name+"上课!");
}
static {//静态代码块
classroom = "五教112";
System.out.println("静态成员方法!");
}
}
注意:
- 静态代码块不管生成多少个对象,其只会执行一次
- 静态成员变量是类的属性,因此是在JVM加载类时开辟空间并初始化的。
- 如果一个类中包含多个静态代码块,在编译的时候,编译器会按照定义的的先后顺序依次执行(合并),静态代码块的执行顺序在实例代码块之前(静态代码块--->实例代码块--->构造方法)。
- 实例代码块只有在创建对象的时候才会执行