我是灼灼,一只初学Java的大一金渐层。
向往余秀华和狄兰·托马斯的疯狂,时常沉溺于将情感以诗相寄;追逐过王尔德、王小波的文字,后陷于毛姆和斯蒂芬·金不可自拔;热爱文学的浪潮,白日梦到底却总在现实里清醒;艳羡平静又极度渴盼奔跑的力量。
欢迎与我交流鸭· QQ:1517526827;
个人博客:https://blog.csdn.net/weixin_52777510?spm=1001.2101.3001.5343
Java笔记——面向对象编程
Java笔记正在连载中,欢迎来其他内容逛逛哟~
相关内容如下:
【连载1】Java笔记——基本结构与流程控制
【连载2】Java笔记——数组操作
【连载3】Java笔记——面向对象编程
【连载4】Java笔记——Java核心类
【连载5】Java笔记——异常处理
【连载6】Java笔记——反射和注解
【连载7】Java笔记——泛型
【连载8】Java笔记——集合
【连载9】MySQL学习笔记
【连载10】JDBC学习笔记
【连载11】Git和GitHub的使用笔记
内容来源于廖雪峰Java教程,加上了自己的一点点理解;
本篇内容内容接上篇~
CATALOGUE–>
2.2方法
2.3构造方法
2.4方法重载
2.5继承
2.6多态
2.7抽象类
2.8接口
2.10包
2.11作用域
2.12内部类
2.14模块
第2章.面向对象编程
2.1面向对象基础
2.1.1class和instance:
class是一种对象模版,它定义了如何创建实例,因此,class本身就是一种数据类型(数据类型的模板)。
instance是对象实例,instance是根据class(模板给的要求)创建的实例(对象),可以创建多个instance,每个instance类型相同(属于一个类的对象),但各自属性可能不相同(各自有各自的特点),多个instance对象实例都拥有模板类class定义的各自的字段field,各自有自己独立的一份数据,互不干扰。
2.1.2定义class(创建类):
类和字段的相互关系:一个class可以包含多个字段field,字段被用来描述一个类的特征,字段是由类class来定义的。
public用于修饰字段,它表示这个字段可以被外部访问。
通过class
,把一组数据汇集到一个对象instance上,实现了数据封装。
举个栗子来分析一下(有点晕):
class Book { //定义类,类名book
public String name; //定义可以公开访问的字段String类型,字段名name,那么以后name就可以被当作一个字段来调用了。
public String author;
public String isbn;
public double price;//定义可以公开访问的字段double类型,字段名price。
}
2.1.3创建实例instance
有模板就要有对象,对象是根据模板来创建的,那么如何创建呢?
利用new操作符来创建一个实例(对象),再定义一个引用类型的变量来指向这个实例。
Person ming//定义person类型的变量 = new Person()//创建person对象;
有了指向这个实例的变量,我们就可以通过这个变量来操作实例。访问实例变量可以用变量.字段
,就是下面写的这种ming.name;ming.age;
访问实例变量ming的字段name并对它赋值写做:
ming.name = "Xiao Ming"; //本质上是在对字段赋值
System.out.println(ming.name);//本质上是在访问字段name
-----(类和对象的相互关系)定义class
就是定义了一种数据类型,对应的instance
是这种数据类型的实例;
class
定义的field
,在每个instance
都会拥有各自的field
,且互不干扰;
通过new
操作符创建新的instance
,然后用变量指向它,即可通过变量来引用这个instance
;
指向instance
的变量都是引用变量(引用类型的变量)。
2.2方法
方法如何写?
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
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;
}
2.2.1什么是方法
一组可以执行任意逻辑的执行语句。
2.2.2为什么要用方法
直接原因:使用private后编译报错,使用方法来使外部变量可以间接修改字段。
直接优势:一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性,方法可以封装一个类的对外接口,调用方无需关心实例内部具体的字段。
2.2.3private的使用
防止外部变量直接访问字段field(把字段用public暴露给外部可能会破坏封装性)
且直接操作field可能会造成逻辑混乱
使用private可以拒绝外部访问
----(利用方法可以检查参数的正确性)虽然外部代码不能直接修改或读取private
字段,但是,外部代码可以调用方法setName()
和setAge()
来间接修改或获取private
字段。在方法内部,我们就有机会检查参数对不对。比如,setAge()
就会检查传入的参数,参数超出了范围,直接报错。这样,外部代码就没有任何机会把age
设置成不合理的值。
对setName()
方法同样可以做检查:不允许传入null
和空字符串;
调用方法的语法是实例变量.方法名(参数);
。例如:ming.setName("Xiao Ming");
。
2.2.4外部代码和方法
应该是类外面定义的方法。
外部代码通过public方法操作实例。
2.2.5内部方法
可以访问private实例,可以调用private方法;
应该指的是类里面定义的方法。
2.2.6方法参数
方法可以包含0个或任意个参数。方法参数用于接收传递给方法的变量值。调用方法时,必须严格按照参数的定义一一传递。
代码栗子:
class Person {
...
public void setNameAndAge//定义方法(String name, int age//按顺序传参) {
...
}
}
调用这个setNameAndAge()
方法时,必须有两个参数,且第一个参数必须为String
,第二个参数必须为int
;
2.2.7可变参数(定义方法时传入的参数)
可变参数用类型...
定义,可变参数相当于数组类型;
public void setNames(String... names) {
this.names = names;
完全可以把可变参数改写为String[]
类型:
public void setNames(String[] names) {
this.names = names;
但是,调用方需要自己先构造String[]
,比较麻烦。
且调用方可以传入null
。而可变参数可以保证无法传入null
,因为传入0个参数时,接收到的实际值是一个空数组而不是null
。
2.2.8方法的基本结构(方法如何定义)
定义方法的语法是:
修饰符//public,private 方法返回类型//void,int String 方法名//setName,getName(方法参数列表Xiao Ming,Xiao Hong {
若干方法语句//this.name=name;
return 方法返回值;
}
方法返回值通过return
语句实现,如果没有返回值,返回类型设置为void
,即不返回任何值,可以省略return
。
2.2.9private方法
和private
字段一样,private
方法不允许外部调用.
定义private
方法的理由是内部方法是可以调用private
方法的。
2.2.10this变量(也是引用变量)
在方法内部,可以使用一个隐含的变量this
,它始终指向当前实例。因此,通过this.field
就可以访问当前实例的字段。****
如果没有命名冲突,可以省略this
。
但是,如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this
;
2.2.11参数绑定
调用方把参数传递给实例方法时,调用时传递的值会按参数位置一一绑定。
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响;引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方。
2.3构造方法
2.3.1构造方法的原因:
由于创建实例的时候,经常需要同时初始化这个实例的字段;
初始化实例通常借助构造方法来完成,因为利用构造方法可以在创建对象实例时就把内部字段全部初始化为合适的值。
2.3.2构造方法的基本格式
代码:
Person p = new Person("Xiao Ming", 15//初始化字段的值,这个值与下面构造方法中的参数一一对应);
public Person//与类名相同(String name, int age//方法参数) {
this.name = name;
this.age = age;
}
构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void
),调用构造方法,必须用new
操作符。
定义实例时传入的参数十分重要,它可以帮助系统正确的识别出对应的构造方法(通过数据类型的对应来区分),这个一定是是一一对应的,就是说在定义多个构造方法时,通过new
操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分。
没有在构造方法中初始化字段时,引用类型的字段默认是null
,数值类型的字段用默认值,int
类型默认值是0
,布尔类型默认值是false
:
class Person {
private String name; // 默认初始化为null,对字段直接初始化用=赋值;
private int age; // 默认初始化为0
public Person() {
}
}
也可以写成这种形式:
Person p = new Person("Xiao Ming", 15//这种对应的是在构造方法中初始化的字段);
如果既对字段进行初始化,又在构造方法中对字段进行初始化:那么构造方法的代码由于后运行,所以,new Person("Xiao Ming", 12)
的字段值最终由构造方法的代码确定。
2.3.3默认构造方法
代码:
public Person{
public Person()//构造方法的名称就是类名{
}
}
如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句.
如果自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法;
2.3.4构造方法与示例的联系
实例在创建时通过new
操作符会调用其对应的构造方法(在圆括号内传入初始化值对应构造方法中的参数),构造方法用于初始化实例;
2.3.5构造方法间的相互关系
在一个构造方法中可以调用其他的构造方法,目的是便于代码的复用。
public Person(String name) {
this(name, 18); // 调用另一个构造方法Person(String, int)
}
public Person() {
this("Unnamed"); // 调用另一个构造方法Person(String),就是他头上的这个方法
}
}
构造方法不可能有void,构造方法可以有多个,方法一定有void之类的东西,方法也可以有多个。
2.4方法重载
2.4.1方法重载的定义:
如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。
这种方法名相同,但各自的参数不同,称为方法重载(Overload
)。
-----方法重载的返回值类型通常都是相同的。
2.4.2方法重载的原因:
功能类似的方法使用同一名字,更容易记住,调用起来更简单。
经典栗子:String
类提供了多个重载方法indexOf()
,可以查找子串:
int indexOf(int ch)
:根据字符的Unicode码查找;int indexOf(String str)
:根据字符串查找;int indexOf(int ch, int fromIndex)
:根据字符查找,但指定起始位置;int indexOf(String str, int fromIndex)
根据字符串查找,但指定起始位置。
2.5继承
2.5.1什么是继承:
继承是面向对象编程的一种强大的机制,它可以实现对代码的复用。
2.5.2继承如何实现:
使用关键字extends来实现继承:
class Student extends Person{
//接下来只需要补充子类额外的功能,而不需要再重复父类的字段和方法;
}
子类自动获得了父类的所有字段,严禁定义与父类重名的字段.
在OOP的术语中,把Person
称为超类(super class),父类(parent class),基类(base class),把Student
称为子类(subclass),扩展类(extended class)。
2.5.3继承的特点:
子类无法访问父类的private字段和方法。
2.5.4父类和子类
1)根父类Object
在Java中,没有明确写extends的类,编译器会自动给它加上extends Object;
2)子类例如Person
子类的子类例如Student
子类的子类例如Teacher
3)单继承
一个子类只能有一个父类(只能继承自一个类),Object类没有父类
任何类除了Object类,都会继承自某个类。
2.5.5继承树
protected被用在继承树里面可以使子类访问父类的方法。protected
关键字可以把字段和方法的访问权限控制在继承树内部,一个protected
字段和方法可以被其子类,以及子类的子类所访问
Object<Person<Student。
2.5.6向上转型upcasting(自动类型转换)
向上转型就是父类类型的变量指向子类类型的实例(对象)P->S,可以把它理解成把子类型的值赋值给父类型的变量;P=S;
把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting);
子类可以自动转型成父类,因为子类具有父类所有的功能,这就完全可以;
向上转型本质上是把子类型安全地变为更抽象的父类型。
因为继承树是Student > Person > Object
,所以,可以把Student
类型转型为Person
,或者更高层次的Object
。
2.5.7向下转型downcasting(强制类型转换)
定义:和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)
父类必须强制转换才可以变成子类,因为子类具有父类所没有的功能;
因此向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException
。
protected
若父类的方法使用它来声明,则子类就可以调用父类的方法,父类不能调用子类的方法;
extends
继承关键字
子类extends父类;
sealed
从Java15开始,允许使用sealed修饰父类,表示被子类继承的这个父类,并通过下面的permits明确写出能够从该类被继承的子类;这种sealed
类主要用于一些框架,防止继承被滥用。
------but sealed类在Java 15中目前是预览状态,要启用它,必须使用参数
–enable-preview和
–source 15`。
permits
表示允许被该父类所继承的所有子类;
final修饰符
正常情况下,只要某个class没有final
修饰符,那么任何类都可以从该class继承。
super关键字
表示父类,通过该关键字可以使子类的构造方法调用父类的构造方法。
子类引用父类的字段时,可以用super.fieldName
;
但其实实际上,使用super.name
,或者this.name
,或者name
,效果都是一样的。编译器会自动定位到父类的name
字段;
-------但要注意:
在Java中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会自动加一句super();
括号内必须传入父类的字段作为参数,如果类本身没有参数的话括号内就不用写了。
如果父类没有默认的构造方法,子类就必须显式调用super()
并给出参数以便让编译器定位到父类的一个合适的构造方法。
-------子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
instanceof操作符
为了避免向下转型时出错;
不能确定是否可以向下转型时的判断方法;
instanceof
实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null
,那么对任何instanceof
的判断都为false
。
-------从Java 14开始,判断instanceof
后,可以直接转型为指定变量,避免再次强制转型,栗子:
public class Main {
public static void main(String[] args) {
Object obj = "hello";
if (obj instanceof String s) {
// 可以直接使用变量s:
System.out.println(s.toUpperCase());
}
}
}
2.5.8继承和组合
因为Student
是Person
的一种,它们是is关系;而Student
和Book
的关系是has关系。
具有has关系不应该使用继承,而是使用组合,即Student
可以持有一个Book
实例:
class Student extends Person {
protected Book book;//has关系的持有
protected int score;
}
因此,继承是is关系,组合是has关系。
2.6多态
2.6.1多态的定义
多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。
-----但是这个实际类型在没有被运行时一般是看不出来的,只看传入的参数类型是不够的,这样无法知道传入参数的实际类型究竟是什么,甚至不知道是不是你所知道的那个子类。所以无法确定调用的究竟是哪一个类的方法。
2.6.2多态的优势
不确定性方法调用的好处???
允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
利用多态,一个方法只需要和一个父类类打交道,它并不需要知道子类以及子类的子类是否存在,就可以让我们得到正确的结果了。如果需要在父类中新增一项子类,只需要从父类中派生,再正确的覆写父类的方法,最后将新的类得到的数据传入方法中就可以了,无需去修改任何代码。
2.6.3多态的特性
**运行期才能动态决定调用的子类方法,对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。**它是一种不确定性的方法调用。
一个非常重要的特性:对象的方法被调用的时候是在进行实际类型的动态调用,这个特性在面向对象编程中称之为多态。英文拼写:Polymorphic。
2.6.4什么是覆写(override)
继承允许子类覆写父类的方法,覆写在子类中改变了父类方法的行为;
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
-----覆写与重载的不同点:
Override和Overload不同的是,如果方法签名不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override
。方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。
所以加上@Override
可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。
2.6.5覆写在多态中的作用
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。栗子:
如果子类和父类中都存在一个相同的方法,且确实是覆写,当发生自动类型转换即向上转型时,实际上调用的应该是子类中的方法,而非父类中被覆写的方法。
2.6.6覆写object方法
所有的class
最终都继承自Object
,而Object
定义了几个重要的方法:
toString()
:把instance对象实例输出为String
;equals()
:判断两个instance是否逻辑相等;hashCode()
:计算一个instance的哈希值。
在必要的情况下,可以覆写Object
的这几个方法,代码:
class Person {
...
// 显示更有意义的字符串:
@Override
public String toString() {
return "Person:name=" + name;
}
// 比较是否相等:
@Override
public boolean equals(Object o) {
// 当且仅当o为Person类型:
if (o instanceof Person) {
Person p = (Person) o;
// 并且name字段相同时,返回true:
return this.name.equals(p.name);
}
return false;
}
// 计算hash:
@Override
public int hashCode() {
return this.name.hashCode();
}
}
调用super(调用就是.)
在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super
来调用。
final修饰符
1)对方法:
继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final
。用final
修饰的方法不能被Override
;
2)对类:
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final
。用final
修饰的类不能被继承;
3)对类的实例字段:
用final
修饰的字段在初始化后不能被修改,对这个字段重新赋值会报错;
--------可以在构造方法(在为什么要构造方法中有提到)中初始化final字段:
class Person {
public final String name;
public Person(String name) {
this.name = name;
}
}
这种方法更为常用,因为可以保证实例一旦创建,其final
字段就不可修改。
2.7抽象类
2.7.1abstract
修饰抽象类
修饰抽象方法
2.7.2如何才能合理的把无意义的父类方法(和执行语句)去掉?
由于定义方法的时候,必须实现方法的语句;去掉父类的方法,就失去了多态的特性。所以不能随便去掉。
2.7.3抽象方法
什么样子呢:如果一个class
定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract
修饰,他只有定义,没有实现。
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法,用abstract来修饰它,这样就表示这是一个抽象方法,它本身(父类的方法)没有任何执行语句。
如果仅仅这样,这个抽象方法本身是无法被执行的,父类也不能被实例化。(编译器报错:无法编译Person
类,因为它包含抽象方法。解决办法:把父类也声明成抽象类,用abstract来修饰它。就可以正常编译了)
-----定义了抽象方法的class必须被定义为抽象类。
2.7.4抽象类
由于仅仅抽象方法的编译无法正常进行,方法无法执行,所以就声明了抽象类。
但是抽象类是无法实例化的,因为抽象所以没有对象;
不过不能实例化的抽象类也是有用哒:由于抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范(一个接口规范)”。
简而言之:抽象类
1)抽象方法相当于是定义了一个规范
2)抽象类的抽象方法没有任何的执行语句
3)子类的方法必须覆写父类的方法,对父类定义的抽象方法加以实现;
4)子类不覆写父类的抽象方法的话,那么子类此时就还是一个抽象类啦。
-------从抽象类继承的子类必须实现抽象方法。
2.7.5面向抽象编程(就是引用抽象类)
定义:一种尽量引用高层类型(父类),避免引用实际子类型的方式;
本质:
- 上层代码(父类的抽象方法)只定义规范(例如:
abstract class Person
); - 不需要子类就可以实现业务逻辑(正常编译);
- 具体的业务逻辑由不同的子类实现,调用者并不关心。
在定义了抽象父类之后,那么父类就可以去引用具体的子类类型的实例了(不可以引用父类本身的实例,因为它根本不能实例化)。
优势是调用者不需要关心父类型变量的具体子类型,就算是引用了一个新的子类型,也还是没有必要关心(冷漠),只关心定义就好。
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。
2.8接口
2.8.1什么是接口
如果一个抽象类没有字段,所有方法全部都是抽象方法, 就可以把该抽象类改写为接口:interface
。(可以把接口看作是一个类似于抽象类但不是抽象类的类)
接口是比抽象类更抽象的纯抽象接口,因为他连字段都没有。
因为接口定义的所有方法默认都是public abstract
的,所以这两个抽象类的修饰符不需要写出来(写不写效果都一样)。
Java的接口(interface)定义了纯抽象规范,一个类可以实现多个接口(可以看作是另一种继承,规范不同)。
接口也是数据类型,适用于向上转型和向下转型,也有继承关系,只不过是与抽象类的。
2.8.2interface
使用interface来声明一个接口;
2.8.3implements关键字
当一个具体的class
去实现一个interface
时,需要使用implements
关键字。栗子:
class Student implements Person (Person是个接口);
-------虽然一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface
,例如:
class Student implements Person, Hello { // 实现了两个interface
...
}
2.8.4接口和抽象类的对比
接口区分:Java的接口特指interface
的定义,表示一个接口类型(更抽象类)和一组方法签名(无字段的一堆抽象方法),而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。
抽象类和接口的对比如下:
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class(子类只能继承自一个父类) | 可以implements(执行)多个interface(具体的一个类可以实现(继承)多个接口) |
字段 | 可以定义实例字段 | 不能定义实例字段 |
抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
2.8.5接口继承
一个interface
可以继承自另一个interface
。interface
继承自interface
使用extends
,它相当于扩展了接口的方法。(可以参照类的继承的关系,但是只是扩展了方法,而不可能有字段)
2.8.6继承关系(抽象类和接口)
一般来说,公共逻辑适合放在abstract class
中,具体逻辑放到各个子类,而接口层次代表抽象程度。
参考Java的集合类定义的一组接口、抽象类以及具体子类的继承关系(合理的设计继承关系可以充分的复用代码):
┌───────────────┐
│ Iterable │
└───────────────┘
▲ ┌───────────────────┐
│ │ Object │
┌───────────────┐ └───────────────────┘
│ Collection │ ▲
└───────────────┘ │
▲ ▲ ┌───────────────────┐
│ └──────────│AbstractCollection │
┌───────────────┐ └───────────────────┘
│ List │ ▲
└───────────────┘ │
▲ ┌───────────────────┐
└──────────│ AbstractList │
└───────────────────┘
▲ ▲
│ │
│ │
┌────────────┐ ┌────────────┐
│ ArrayList │ │ LinkedList │
└────────────┘ └────────────┘
在使用的时候,实例化的对象永远只能是某个具体的子类(抽象类不能实例化,接口自然也不能),但总是通过接口去引用它,因为接口比抽象类更抽象:
List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口
2.8.7default方法(jdk>=1.8)
仅能在接口中定义并实现他,把default看作是一个修饰符;实现类可以不必覆写default
方法。default
方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default
方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
和抽象类的普通方法对比:
default方法不能访问实例字段(接口没有字段),抽象类普通方法可以访问实例字段。
2.9静态字段和静态方法
2.9.1静态字段
在一个class
中定义的字段,称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响;还有一种字段,是用static
修饰的字段,称为静态字段:static field
。
对比:实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段,本身属于类;虽然实例可以访问静态字段,但是它们指向的都是类的静态字段,由于在同一个类里面,所以会共享。
访问静态字段时写做class名.static field,写成实例变量名.field会得到编译器的警告(原因:实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段
来访问静态对象),故不推荐;
无论修改哪个实例的静态字段的值,所有实例的静态字段的值都会被修改,因为静态字段本身不属于实例,实例字段并没有静态变量,静态字段属于它所在的类(可以把静态字段理解为描述class
本身的字段(非实例字段);应该不是属于的关系)。
2.9.2静态方法
用static修饰,他本身属于类;
调用实例方法必须通过一个实例变量(引用类型变量的指向),而调用静态方法则不需要实例变量,通过类名就可以调用(类名.静态方法名;规范同静态字段);静态方法类似其它编程语言的函数。
(区分清楚访问和调用)因为静态方法属于class
而不属于实例,因此,静态方法内部,无法访问this
变量,也无法访问实例字段,它只能访问静态字段和其他静态方法,不能调用其他字段;
静态方法常用于工具类:
- Arrays.sort()
- Math.random()
静态方法也经常用于辅助方法,注意到Java程序的入口main()
也是静态方法。
2.9.3接口
纯抽象类,不能定义实例字段,所以没有其他字段,但是可以有静态字段----必须用final修饰;
实际上接口的字段修饰符public final static可以省略掉(因为只能是这种类型,这个样子),编译器会自动把省略的字段都补好;
字段名全部是大写字母;
2.10包
2.10.1包package为什么会存在?
为了解决类的命名冲突问题,如果把包放在类的上面一级,那么相同的类名但是不同的包名就可以认为是两个不同的类了,因为完整的类名应该写作–包名.类名–。
Java定义了一种名字空间,称之为包:package
。一个类总是属于某个包,类名(比如Person
)只是一个简写,完整的应如上。
2.10.2包的命名
声明语句:在定义class
的时候,我们需要在第一行声明这个class
属于哪个包。
在Java虚拟机执行的时候,JVM只看完整类名,所以只要包名不同,类就不同。
包可以是多层结构,用.
隔开。例如:java.util
但要注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
没有定义包名的class
,它使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法(就是一定要写包名啦)。
栗子:把package Sample作为根目录,src作为源码目录,新建项目的类的时候,在class的框框里应该写成:com.Sample.Hello.java;所有Java文件对应的目录层次要和包的层次一致;
------有关编译后的命令:
编译后的.class
文件需要按照包结构存放。如果使用IDE,把编译后的.class
文件放到bin
目录下,那么,编译的文件结构就是(资料里的例子):
package_sample
└─ bin
├─ hong
│ └─ Person.class
│ ming
│ └─ Person.class
└─ mr
└─ jun
└─ Arrays.class
编译的命令相对比较复杂,需要在src
目录下执行javac
命令:
javac -d ../bin ming/Person.java hong/Person.java mr/jun/Arrays.java
在IDE中,会自动根据包结构编译所有Java源码,所以不必担心使用命令行编译的复杂命令。
-----命名实践:
为了避免名字冲突,需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:
- org.apache
- org.apache.commons.log
子包就可以根据功能自行命名。
要注意不要和java.lang
包的类重名,即自己的类不要使用这些名字:
- String
- System
- Runtime
- …
要注意也不要和JDK常用类重名:
- java.util.List
- java.text.Format
- java.math.BigInteger
- …
2.10.3包的作用域
定义:包以内不用public,private,protected修饰的字段和方法;位于同一个包的类,可以访问包作用域的字段和方法。
import:
导入包的关键词;
用于在一个类中引用其他的类;
包和类的关系
类名全称应该是:包名.类名
在一个类中引用其他的类的方法
1)直接引用:直接写出完整类名;
2)import语句:导入其他的类的完整类名,再写上简单的类名;
在写import
的时候,可以使用*
,表示把这个包下面的所有class
都导入进来(但不包括子包的class
);一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays
类属于哪个包。
3)import static
的语法:它可以导入一个类的静态字段和静态方法;但是这个很少被使用;
2.10.4如何正确的查找类名(这是编译器要做的事)
我们看结果就好,Java编译器最终编译出的.class
文件只使用完整类名,因此,在代码中,当编译器遇到一个class
名称时:
- 如果是完整类名,就直接根据完整类名查找这个
class
; - 如果是简单类名,按下面的顺序依次查找:
- 查找当前
package
是否存在这个class
; - 查找
import
的包是否包含这个class
; - 查找
java.lang
包是否包含这个class
。
- 查找当前
如果按照上面的规则还无法确定类名,则编译报错。
-----编写class的时候,编译器会自动帮我们做两个import动作:
- 默认自动
import
当前package
的其他class
; - 默认自动
import java.lang.*
。
注意:自动导入的是java.lang包,但类似java.lang.reflect这些包(不是同一个包,也不在它里面)仍需要手动导入。
如果有两个class
名称相同,例如,mr.jun.Arrays
和java.util.Arrays
,那么只能自动地import
其中一个,另一个必须写完整类名。
2.10.5Java内建的包
目的是为了避免命名冲突;
JDK的核心类使用java.lang
包,编译器会自动导入;
JDK的其它常用类定义在java.util.*
,java.math.*
,java.text.*
,……;
2.11作用域
public
、protected
、private
这些修饰符在Java中可以用来限定访问作用域。
2.11.1内建的访问权限public
定义为public
的class
、interface
可以被其他任何类,包括其他包的任何类访问;
定义为public
的field
、method
可以被其他类访问和调用,前提是首先有访问class
的权限;
如果不确定是否需要public
,就不声明为public
,即尽可能少地暴露对外的字段和方法。
2.11.2 内建的访问权限private
定义为private
的field
、method
无法被其他类访问;private
访问权限被限定在class
的内部,而且与方法声明顺序无关。应该把private
方法放到后面,因为public
方法定义了类对外提供的功能,阅读代码的时候,应该先关注public
方法;
如果一个类内部还定义了嵌套类,则嵌套类拥有访问private
的权限;
2.11.3嵌套类
定义在一个class
内部的class
称为嵌套类(nested class
),Java支持好几种嵌套类。
2.11.4内建的访问权限protected
protected
作用于继承关系。定义为protected
的字段和方法可以被子类访问,以及子类的子类(包名必须完全一致);
2.11.5内建的访问权限package
上节有提到;
只要在同一个包,就可以访问package
权限的class
、field
和method
;
把方法定义为package
权限有助于测试,因为测试类和被测试类只要位于同一个package
,测试代码就可以访问被测试类的package
权限方法。
2.11.6局部变量
在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的一个块结束。方法参数也是局部变量。
使用局部变量时,应该尽可能把局部变量的作用域缩小,尽可能延后声明局部变量。
2.11.7final修饰符
final
与访问权限不冲突,它有很多作用;之前有写过;
补充:用final
修饰局部变量可以阻止被重新赋值;
------一个.java
文件只能包含一个public
类,如果有public
类,文件名必须和public
类的名字相同。
2.12内部类(看不懂-_-)
内部类(被定义在一个类的内部,所以称为内部类(Nested Class))和上面写的嵌套类相似;
Java的内部类可分为Inner Class、Anonymous Class和Static Nested Class三种:
2.12.1outer class:inner class
如果一个类定义在另一个类的内部,这个类就是Inner Class;而outer class是一个普通类;
内部类的实例不能单独的存在,必须依附于外部类的实例,实例化一个inner的步骤栗子:
1)创建一个outer实例:
Outer outer = new Outer(“Nested”);
2)调用outer类的实例来创建一个inner实例
Outer.Inner inner = outer.new Inner();
关于实例化inner时不能脱离outer实例的原因:
因为Inner Class除了有一个this
指向它自己,还隐含地持有一个Outer Class实例,可以用Outer.this
访问这个实例。
inner class可以引用outer实例和访问outer class的private方法和字段(因为Inner Class的作用域在Outer Class内部);
------Java编译器编译后的.class
文件中,Outer
类被编译为Outer.class
,而Inner
类被编译为Outer$Inner.class
。
2.12.2outer class:Anonymous class匿名类
这种不需要在Outer Class中明确地定义这个Class,而是在方法内部,通过匿名类(Anonymous Class)来定义,栗子:
void asyncHello() {
Runnable r = new Runnable() ;//在方法内部实例化了一个runnable,runnable本身是一个接口,但是接口实际上并不能实例化,所以这里实际上是定义了一个实现接口的匿名类,并且通过new来实例化该匿名类,然后转型为runnable;
-------在定义匿名类的时候就必须实例化它,定义匿名类的写法如下:
Runnable r = new Runnable() {
// 实现必要的抽象方法...
};//因为接口本身是更抽象的纯接口,内部全部是抽象方法
必须依附于外部类的实例来创建实例
可以访问outer class的private方法和字段
定义匿名类的原因:在这种定义匿名类转型为接口的情况中,并不关心类名,比直接定义前一种inner class要简洁的多;
可以由接口继承出来;也可以继承自普通类;(这里看不懂)资料:
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, String> map1 = new HashMap<>();
HashMap<String, String> map2 = new HashMap<>() {}; // 匿名类!
HashMap<String, String> map3 = new HashMap<>() {
{
put("A", "1");
put("B", "2");
}
};
System.out.println(map3.get("A"));
}
}
map1
是一个普通的HashMap
实例,但map2
是一个匿名类实例,只是该匿名类继承自HashMap
。map3
也是一个继承自HashMap
的匿名类实例,并且添加了static
代码块来初始化数据。观察编译输出可发现Main$1.class
和Main$2.class
两个匿名类文件。
2.12.3static nested class
用static
修饰的内部类虽然与inner class看起来很相似,只是多了静态修饰符,但其实它和Inner Class有很大的不同,它不再依附于Outer
的实例,而是一个完全独立的类,因此无法引用Outer.this
,但它可以访问Outer
的private
静态字段和静态方法。如果把StaticNested
移到Outer
之外,就失去了访问private
的权限(那就不叫内部类了);
本节总结为:
- Inner Class和Anonymous Class本质上是相同的,都必须依附于Outer Class的实例,即隐含地持有
Outer.this
实例,并拥有Outer Class的private
访问权限; - Static Nested Class是独立类,但拥有Outer Class的
private
访问权限;
2.13classpath和jar
2.13.1class path究竟是什么
1)classpath
是JVM用到的一个环境变量,它用来指示JVM如何搜索class
;因为Java是编译型语言,源码文件是.java
,而编译后的.class
文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,如果要加载一个abc.xyz.Hello
的类,应该去哪搜索(如何搜索:路径和顺序如何)对应的Hello.class
文件。
2)classpath
就是一组目录的集合,它设置的搜索路径与操作系统相关。例如,在Windows系统上,用;
分隔(就是把两个路径分开,分次分顺序查找),带空格的目录用""
括起来,可能长这样:
C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"
在Linux系统上,用:
分隔,可能长这样:
/usr/shared:/usr/local/bin:/home/liaoxuefeng/bin
----.
代表当前目录;如果JVM在某个路径下找到了对应的class
文件,就不再往后继续搜索。如果所有路径下都没有找到,就报错。
2.13.2如何配置(设定)环境变量
配置环境变量的方法(两种):
不要在外部配置环境变量(因为那样会污染整个系统环境),就是指在系统环境变量(一开始配置bin的那个地方)中设置classpath
环境变量;
推荐在启动jvm时配置环境变量;
命令行中使用(就是第二种设定方法通过-cp命令传入):
就是给java
命令传入-classpath
或-cp
参数:
java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello
或者使用-cp
的简写:
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
没有设置系统环境变量,也没有传入-cp
参数,那么JVM默认的classpath
为.
,即当前目录:
java abc.xyz.Hello
上述命令告诉JVM只在当前目录搜索Hello.class
。
在IDE中运行Java程序,IDE自动传入的-cp
参数是当前工程的bin
目录和引入的jar包。
2.13.3有关配置环境变量方法命令的错误认识
在我们自己编写的class
中,会引用Java核心库的class
,例如,String
、ArrayList
等。这些class
应该去哪里找?
有很多“如何设置classpath”的文章会告诉你把JVM自带的rt.jar
放入classpath
,但实际上根本不需要告诉JVM如何去Java核心库查找class
;
--------不要把任何Java核心库添加到classpath中,JVM根本不依赖classpath来加载核心库;
更好的做法是,不要设置classpath
;默认的当前目录.
对于绝大多数情况都够用了。
2.13.4什么是jar包
jar包是用来把各层目录打一个包(它是一个zip格式的压缩文件,相当于目录),变成一个文件,从而方便把散落在各层目录中的.class文件整理起来的;它可以把package
组织的目录层级,以及各个目录下的所有文件(包括.class
文件和其他文件)都打成一个jar文件,这样就会很方便;
如果我们要执行一个jar包的class
,就可以把jar包放到classpath
中(把环境变量和jar包联系起来):
java -cp ./hello.jar abc.xyz.Hello
这样JVM会自动在hello.jar
文件里去搜索某个类。
2.13.5如何创建jar包
因为jar包就是zip包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip
改为.jar
,一个jar包就创建成功;
---------jar包里的第一层目录,不能是bin
(就是不能包括bin包),应该是具体的自己定义的包名;
2.13.6jar包的包含
包含一个特殊的/META-INF/MANIFEST.MF
文件,MANIFEST.MF
是纯文本,可以提供Main-Class
和其它包的信息。JVM会自动读取这个MANIFEST.MF
文件,如果存在Main-Class
,就不必在命令行指定启动的类名,而是用更方便的命令去直接运行jar包:
java -jar hello.jar
jar包还可以包含其它jar包,这个时候,就需要在MANIFEST.MF
文件里配置classpath
了。
在大型项目中,不可能手动编写MANIFEST.MF
文件,再手动创建zip包。Java社区提供了大量的开源构建工具,例如Maven,可以非常方便地创建jar包。
2.14模块(本节很复杂,再看看资料吧-_-)
2.14.1模块是如何产生的(历史)
从Java 9开始,JDK引入了模块(Module),注意之前的版本没有;
因为.class
文件是JVM看到的最小可执行文件,而一个大型程序需要编写很多Class,并生成一堆.class
文件,很不便于管理,所以,jar
文件就是class
文件的容器,前面的jar包是起一个目录的作用。
在Java 9之前,一个大型Java程序会生成自己的jar文件,同时引用依赖的第三方jar文件,而JVM自带的Java标准库,实际上也是以jar文件形式存放的,这个文件叫rt.jar
,一共有60多M。
如果是自己开发的程序,除了一个自己的app.jar
以外,还需要一堆第三方的jar包,运行一个Java程序,一般来说,命令行写这样:
java -cp app.jar:a.jar:b.jar:c.jar com.liaoxuefeng.sample.Main
------JVM自带的标准库rt.jar不要写到classpath中,写了反而会干扰JVM的正常运行。
如果漏写了某个运行时需要用到的jar,那么在运行期极有可能抛出ClassNotFoundException
。
jar只是用于存放class的容器,它并不关心class之间的依赖。
从Java 9开始引入的模块,主要是为了解决“**依赖”**这个问题(模块的目的)。如果a.jar
必须依赖另一个b.jar
才能运行,那我们应该给a.jar
加点说明,让程序在编译和运行的时候能自动定位到b.jar
,这种自带“依赖关系”的class容器就是模块(模块的定义)。
为了表明Java模块化的决心,从Java 9开始,原有的Java标准库已经由一个单一巨大的rt.jar
分拆成了几十个模块,这些模块以.jmod
扩展名标识,可以在$JAVA_HOME/jmods
目录下找到它们:
- java.base.jmod
- java.compiler.jmod
- java.datatransfer.jmod
- java.desktop.jmod
- …
这些.jmod
文件每一个都是一个模块,模块名就是文件名。例如:模块java.base
对应的文件就是java.base.jmod
。模块之间的依赖关系已经被写入到模块内的module-info.class
文件了。所有的模块都直接或间接地依赖java.base
模块,只有java.base
模块不依赖任何模块,它可以被看作是“根模块”,好比所有的类都是从Object
直接或间接继承而来。
把一堆class封装为jar(jar包)仅仅是一个打包的过程,而把一堆class封装为模块(module)则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的JVM提供不同的版本。
2.14.2模块如何编写(资料在链接中)
2.14.3模块如何运行(资料在链接中)
2.14.4JRE的打包
使用模块可以按需打包JRE;
2.14.5jlink命令
复制一份jre,但是只携带有用的一部分模块;
2.14.6访问权限进阶版
引入模块后,之前的访问权限的规则要稍微做些调整;因为class的这些访问权限只在一个模块内有效,模块和模块之间,例如,a模块要访问b模块的某个class,必要条件是b模块明确地导出(exports)了可以访问的包。
所以说,模块进一步隔离了代码的访问权限。
2.15如何理解面向对象编程:
面向对象编程,是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。
英文:
Object-Oriented Programming,简称OOP。
面向对象的基本概念
类
实例(对象)
方法
面向对象的实现方式
继承
多态
Java语言本身提供的机制
package
classpath
jar
Java标准库提供的核心类
字符串
包装类型
JavaBean
枚举
常用工具类
因为里面添加了自己的一些想法,所以可能会有错误,请大家及时的联系我修改~
如果对你有帮助的话不要忘记一键三连噢~
谢谢鸭~
初次编写于2020/12/14日;
一改于2021/1/23日。