Java基础——面向对象和类
程序员文章站
2022-05-25 20:49:45
...
这篇文章不是面面俱到的基础知识集合,只是我个人的学习笔记。说人话就是:这篇文章是JavaSE基础知识全解的真子集。且以下所有内容仅代表个人观点,不一定正确。欢迎辩证~
先从简化的Java内存模型开始
- 这是一个简化版的Java内存模型。该模型中把内存分为了栈区、堆区、方法区。
栈区
- 主要用于存放方法执行时的各种基本数据类型和引用数据类型。
- 例如图中,main方法中声明了引用类型p,那么栈区中就会为p申请一块内存。
- 对于基本数据类型和引用数据类型,他们在栈区中存储的内容是不一样的:
基本数据类型的存储
- 基本数据类型在栈区中存储的是值。并且对于相同的值,基本数据类型共用一块内存。什么意思呢?看下图:
- 像"1"这样的基本数据类型的值,我们称为直接量。int b = 1;时,会先查找栈中是否存在直接量"1",如果找到了,那么直接使用这个直接量。
- 所以真正存储在栈区中的其实是直接量,相同的直接量,不会重复占用多个内存空间,这就是上面代码a和b使用同一地址的原因。而不同的值会被分配到不同的地址去,所以如果a=1、b=2,那么他们的地址又是不同的。
- 不同类型的相同值,也是会被分配到不同地址,很好理解嘛,因为他们需要的内存大小都不一样嘛。比如1.0和1.0F,他们的在栈中的地址是不一样的。
- 尽管这个特性看起来很像是“引用”,但是我们需要避免这样去称呼他们。因为“引用”指的是利用栈区中的内存地址,指向堆区中的数据。
引用数据类型的存储
- 学过C的就很容理解引用类型的存储方式。没错,就是很像指针~
- 引用数据类型本身存储的是指向堆区的地址(实际上存的是地址的hash)。而引用的对象的真正的值是存放在堆区中的。
堆区
- 堆区用于存放对象的实例(通俗的说法的是存放new出来的东西)。例如数组、实例化后的成员变量等。
- 引用数据类型的值就是堆区中某个实例的地址hash。
方法区
- 方法区存放编译后的代码、各类静态资源等。
面向对象要点(没对象的好好学、有对象的好好看)
类基础
方法中的不定长参数
- 这个东西似乎很少用到,所以可能很多人不知道。如果入参声明为不定长参数,那么调用该方法的时候,可以传入0-N个该类型的实参。举例如下:
public class Test{
public static void main(String[] args){
Test test = new Test();
test.testPara("1");
test.testPara("1", 1);
test.testPara("1", 2, 3);
}
public void testPara(String str, int... args){
System.out.println(args.length);
}
}
- 上面代码中,main方法对testPara的三次调用都是可行的。也就是的0-N个该类型参数都可以。
- 此外,可以很显然地看见,不定长参数其实就是一个数组。但是形参写
int... args
和写int[] args
还是会有很大不一样。具体的区别可以自己琢磨琢磨。 - 最后,最重要的一点是,如果要使用不定长参数,那么参数列表中只能存在一个不定长参数。并且该不定长参数必须放在形参列表的最后面。
这就是this?
返回一个this试试?
- 相信小学毕业的小伙伴们都知道this的意思是“当前对象”,所以它本质上是一个对象(实例)的引用。也正因为如此,this也是可以作为返回值的噢。
class A{
A getA(){
return this;
}
public static void main(String[] args){
A a1 = new A();
A a2 = a1.getA();
//A a2 = a1;
}
}
- 如上代码,两行声明a2的代码,执行结果是一模一样的。希望通过这个例子,能让小学2年级的小伙伴们更深刻地this代表的“当前对象”是什么意思。
this的特殊用法
- this可以用于构造方法中调用同一个类中的另一个构造函数。这个时候可能就没法用this代表“当前对象”来理解了,所以我将其称为特殊用法。举例如下:
class A {
int num;
int sum;
A(int num) {
this.num = num;
System.out.println("这是无参构造");
}
A(int num, int sum) {
this(num);
this.sum = sum;
System.out.println("这是有参构造");
}
A getA() {
return this;
}
public static void main(String[] args) {
A a1 = new A(1, 2);
A a2 = a1.getA();
System.out.println("" + a2.num + a2.sum);
}
}
- 上面代码的执行结果是12。而
this(num);
就是刚提到的用this调用构造方法。值得注意的是,该语句只能放在构造方法的第一行,否则直接编译报错。 - 这个用法应该几乎不会用到,但是类似的关键字super可是会经常这样用的哦~这个下文会提到
看完之后就想静静的静态关键字static
- 说到static,大家都会说:哎呀~就是修饰成员变量和成员方法嘛,让它变成类变量、类方法嘛。但是喃,我发现static在一些地方的用法,之前被我忽略咯。
static与JavaBean
- 我们知道JavaBean是Java封装的产物,其主要由私有成员变量、getter和setter方法组成。不过我们平时用到的几乎都是对象层级的JavaBean,也就是new出来对象之后,再来使用。
- 但其实JavaBean也可以在类层级上,使用static实现。以满足需要全局唯一JavaBean的需求。举例如下:
public class JavaBean {
private int normal;
private static int sta;
public void print() {
System.out.println("sta:" + sta + ". normal:" + normal);
}
public static void staticPrint(){
System.out.println("sta:" + sta);
//System.out.println("normal:" + normal);
}
public int getNormal() {
return normal;
}
public void setNormal(int normal) {
this.normal = normal;
}
public static int getSta() {
return sta;
}
public static void setSta(int sta) {
JavaBean.sta = sta;
}
public static void main(String[] args){
JavaBean.setSta(21);
JavaBean.staticPrint();
JavaBean javaBean = new JavaBean();
javaBean.print();
}
}
- 执行结果为:
sta:21
sta:21. normal:0
- 通过这种方式,能够实现对全局唯一的变量(静态变量)的封装。并且有两个点需要注意一下:
- 要封装静态成员,那么所有涉及到该成员的方法都应该是静态的(好废话哦)。例如setter和getter。
- 别忘了静态方法里面可不能使用实例成员哦(还是好废话哦)。例如被我注释掉的代码。
- 有人就问了,这个东西有啥用呢?别着急,接下来就是大名鼎鼎的设计模式之一:单例模式。
单例模式
什么是单例模式
- 单例模式很好理解,其实就是通过一定的封装,让一个类在使用的时候,始终只会有一个实例。
- 单例模式可进一步分为饿汉式和懒汉式。
- 饿汉式在类加载时就进行初始化。
- 懒汉式在get方法被初次调用的时候再进行初始化。
- 实际开发中推荐使用饿汉式,原因和多线程的问题有关。
单例模式的实现步骤
- 私有化构造方法;
- 声明本类类型的引用指向本类类型的对象,并使用private static;
- 提供公有的get方法返回引用变量,使用public static。
举例
//懒汉式
public class House {
private String owner;
private static House house = null;
private House() {
System.out.println("被创建了");
}
public static House getInstance() {
if (null == house) {
house = new House();
}
return house;
}
public String getOwner() {
return owner;
}
public static House getHouse() {
return house;
}
public void setOwner(String owner) {
this.owner = owner;
}
}
//饿汉式
public class House {
private String owner;
private static final House house = new House();
private House() {
System.out.println("被创建了");
}
public static House getInstance() {
return house;
}
public String getOwner() {
return owner;
}
public static House getHouse() {
return house;
}
public void setOwner(String owner) {
this.owner = owner;
}
}
//测试类
public class SingletonTest {
public static void main(String[] args) {
House s1 = House.getInstance();
House s2 = House.getInstance();
s1.setOwner("angel");
s2.setOwner("小钰");
System.out.println(s1.getOwner());
System.out.println(s2.getOwner());
System.out.println(s1 == s2); // true
}
}
- 执行结果为
被创建了
小钰
小钰
true
- 可以发现s1和s2引用的对象是相同的,这就是单例模式的特点。我们最常用到的Windows的任务管理器,就是一个典型的单例对象。
内存分析
- 首先java编译后,方法字节码会存放在方法区。同时存放在方法区的还有一个静态引用变量house。
- 同时执行main方法的时候,方法中的局部变量会存放在栈区中。
- 而new出来的House会存放在堆区中。所以此时引用变量house会指向堆区中的House实例。
- 而引用变量s1和s2,获取到的其实都是house的值,也就是说他们也都是指向的同一个House实例。
构造块和静态代码块
- 在类中直接使用{}括起来的一段代码,叫做构造块。如果在前面用static修饰,则叫做静态代码块。两者有什么用呢?直接看例子:
public class Person {
private static int legs;
private String name;
//代码块,在每次生成对象实例之前执行。先于构造方法执行
{
System.out.println("生成一个人");
}
//静态代码块,在类加载的时候执行。先于构造块执行
static {
System.out.println("加载人类");
Person.legs = 2;
}
public Person(String name) {
System.out.println("实例化了一个人");
setName(name);
}
public static int getLegs() {
return legs;
}
public static void setLegs(int legs) {
Person.legs = legs;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void show() {
System.out.println("我的名字叫" + this.name + "。我有" + Person.legs + "条腿");
}
}
//测试类
public class Test {
public static void main(String[] args){
Person p1 = new Person("angel");
Person p2 = new Person("小钰");
p1.show();
p2.show();
}
}
- 执行结果是
加载人类
生成一个人
实例化了一个人
生成一个人
实例化了一个人
我的名字叫angel。我有2条腿
我的名字叫小钰。我有2条腿
- 不难发现。构造块会在每次执行构造方法之前执行,而静态代码块只会在第一次加载类的时候执行。
- 因此,一般构造块用于在构造方法执行前,对某些变量进行统一赋值;静态代码块一般用于对静态成员,实现类似于“构造方法”一样的效果。
- 如果一个方法中写了多个构造块,则执行时,按照从上到下的顺序依次执行。静态代码块同理。
终于到final了
- final关键字平时接触得挺多的,这里大概提一下就好了:
- 修饰类:表示该类无法被继承。
- 修饰方法:表示该类的子类中,不能重写该方法。
- 修饰变量:表示变量只能被初始化,不能被赋值。
- final经常和static一起用在成员变量上,从而实现“常量”的功能。
访问控制
- 访问控制大家也比较了解,不多说,推荐一张表格,看完之后身心舒畅。
修饰符 | 本类 | 同一个包中的类 | 子类 | 其他类 |
---|---|---|---|---|
public | 可以访问 | 可以访问 | 可以访问 | 可以访问 |
protected | 可以访问 | 可以访问 | 可以访问 | 不能访问 |
默认 | 可以访问 | 可以访问 | 不能访问 | 不能访问 |
private | 可以访问 | 不能访问 | 不能访问 | 不能访问 |
封装
- java的一个特性。这个特性很简单,就不展开讲了。一句话就是私有化成员,提供getter和setter。
继承
- java的第二个特性
继承的几大特点
老汉的遗产并不是全给娃娃的噢
- 子类不能继承父类的构造方法。即子类的实例不能调用父类的构造方法。这句话其实有点废话,因为子类的类名和父类就不一样呀。当然,父类的构造方法在子类的构造方法中通过super访问。
- 私有成员变量和方法不能被直接继承,但是可以间接访问。举个栗子来说明一下:
public class Person {
private String name;
public int height;
private String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void show() {
System.out.println("我的名字叫:" + this.getName() + "。身高为:" + this.height);
}
}
public class Girl extends Person {
}
public class Test {
public static void main(String[] args){
Girl girl = new Girl();
//girl.name;
//girl.getName();
girl.height = 10;
girl.setName("angel");
girl.show();
}
}
- 运行测试类的main方法,结果是
我的名字叫:angel。身高为:10
- 也就是说,子类虽然不能直接继承父类的私有成员和私有方法,但是这些成员和方法在堆区中是存在的,可以通过继承父类的其他方法来间接访问。
子类如何使用父类的构造方法?
- 子类的构造方法被调用时,默认都会自动调用父类的无参构造方法,来初始化从父类中继承的成员变量。相当于在子类构造方法的第一行写上
super();
的效果。 - 同时,也可以显示地调用父类的构造方法,只需要在子类构造方法中的第一行使用
super();
即可,实参列表可根据父类构造方法自行填写。举个栗子:
public class Person {
private String name;
public Person(String name) {
setName(name);
System.out.println("调用了Person(String name)");
}
public Person() {
System.out.println("调用了Person()");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void show() {
System.out.println("我的名字叫:" + this.getName());
}
}
public class Girl extends Person {
public Girl(){
}
public Girl(String name){
super(name);
}
}
public class Test {
public static void main(String[] args){
Girl yu = new Girl("小钰");
Girl angel = new Girl();
angel.setName("angel");
yu.show();
angel.show();
}
}
- 执行结果是
调用了Person(String name)
调用了Person()
我的名字叫:小钰
我的名字叫:angel
- 阔以看到,即使调用无参构造
Girl();
,也依然会调用Person();
。 - 并且可以通过
super(name);
来构造父类中的私有成员变量。
方法重写
- 方法重写大家都经常用,这里提一下有几个原则:
- 方法名、参数列表必须相同。
- 当且仅当重写方法的返回类型是父类方法的返回类型的子类时,返回值类型可以不同。
- 访问权限必须大于等于父类方法。
- 抛出的异常必须等于或小于父类的异常。比如父类抛NullPointException,则子类不能抛Exception。反之可以。
继承中的构造块
- 当构造块和静态代码块遇上继承之后,是什么结果呢?
- 不举栗子了,直接上结论。执行的先后顺序是:
- 父类的静态代码块;
- 子类的静态代码块;
- 父类的构造块;
- 父类的构造方法;
- 子类的构造块;
- 子类的构造方法。
多态
- 多态是Java的第三大特性,相信各位已入门的小伙伴是能够知道多态的作用滴。简要来说就是让一个父类的引用指向不同的子类,从而在不同的情况下产生不同的效果。
- 当父类类型的引用指向子类类型的对象时(多态),方法调用会有以下特点:
- 父类类型的引用可以直接调用父类独有的方法。
- 父类类型的引用不可以直接调用子类独有的方法。需要强制类型转换。
- 动态绑定:对于父子类都有的非静态方法来说,编译阶段编译父类的方法,运行阶段调用子类重写的版本。
- 对于父子类都有的静态方法,编译和运行阶段都调用父类版本。也就是说静态方法不存在多态的特性。至于调用哪个方法,之和引用类型本身有关。
- 下面按顺序详细说明一下多态的这四种特点。
多态核心——重写父类方法并通过父类引用调用
- 这个可能是大家平时最常用到的多态的特性。这里给个栗子感受一下就好:
public class Person {
public void introduce(){
System.out.println("俺是一个人");
}
}
public class Boy extends Person {
@Override
public void introduce(){
System.out.println("我是男孩,帅气的男孩!");
}
}
public class Girl extends Person {
@Override
public void introduce(){
System.out.println("我是女孩,漂亮的女孩~");
}
}
public class Test {
public static void main(String[] args){
Person person = new Girl();
person.introduce();
person = new Boy();
person.introduce();
}
}
- 运行结果是。可以发现,同一个父类引用person的同一个方法,可以产生不同的结果。
我是女孩,漂亮的女孩~
我是男孩,帅气的男孩!
引用类型的类型转换
- 引用数据类型之间的转换方式有两种:自动类型转换和强制类型转换。
- 自动类型转换在子类转父类时触发,所以父类引用指向子类时,并不需要强转。
- 强制类型转换在父类转子类时使用,强转之后父类可以调用子类的独有方法。
- 非常重要的一点:强转的目标类型与该引用指向的类型不一样时,虽然编译能通过,但运行时会抛出ClassCast异常。
- 为了避免这一点,可以使用
引用变量 instanceof 数据类型
,当引用变量指向的数据类型和instanceof的数据类型相同时,表达式返回true,否则返回false。
- 为了避免这一点,可以使用
- 小小验证一下,在上一个测试类下面追加几行代码:
public class Test {
public static void main(String[] args){
Person person = new Girl();
person.introduce();
person = new Boy();
person.introduce();
System.out.println("验证一下类型转换");
person = new Girl();
Girl girl = (Girl)person;
Boy boy = (Boy)person;
}
}
- 执行结果如下。可以看到因为person指向的是girl的实例,所以强转为boy是会出现ClassCast异常。
我是女孩,漂亮的女孩~
我是男孩,帅气的男孩!
验证一下类型转换
Exception in thread "main" java.lang.ClassCastException: class com.ObjectOriented.Polymorphism.Girl cannot be cast to class com.ObjectOriented.Polymorphism.Boy (com.ObjectOriented.Polymorphism.Girl and com.ObjectOriented.Polymorphism.Boy are in unnamed module of loader 'app')
at com.ObjectOriented.Polymorphism.Test.main(Test.java:15)
- 然后修改为如下,就不会报错了:
public class Test {
public static void main(String[] args){
Person person = new Girl();
person.introduce();
person = new Boy();
person.introduce();
System.out.println("验证一下类型转换");
person = new Girl();
Girl girl = (Girl)person;
if(person instanceof Boy){
Boy boy = (Boy)person;
}
}
}
动态绑定
- 动态绑定的意思上面已经解释过了,这里再举例感受一下什么叫动态绑定~
public class Person {
/*public void introduce(){
System.out.println("俺是一个人");
}*/
}
- 接上面的例子,其他所有东西都不改,就把Person类中的introduce()方法注掉。大家会发现Test类中的
person.introduce();
的编译就报错了。 - 相反,如果你只注释掉Girl类中的introduce,就不会有问题。因为相当于没有override嘛,就变成了调用Person类的introduce。
- 这就是动态绑定产生的结果。大家都知道,我们实际运行的时候,执行的其实是Girl类中的introduce,但是编译阶段却只关心Person类中有没有introduce。至于运行的时候的introduce,是运行时才动态“赋予”的。
静态与多态
- 这一节其实我都不想写,上面说得蛮清楚的,但是不写的话就破坏队形了=,=
- 但还是举个例子吧哈哈哈
public class Person {
public static void introduce(){
System.out.println("俺是一个人");
}
}
- 其他都不变,就把Person类的introduce方法改为静态。就会发现,哇!@Override报错了,好的,那就把@Override都去掉,顺便把Girl类和Boy类的introduce方法也改为静态。
public class Girl extends Person {
public static void introduce(){
System.out.println("我是女孩,漂亮的女孩~");
}
}
public class Boy extends Person {
public static void introduce(){
System.out.println("我是男孩,帅气的男孩!");
}
}
public class Test {
public static void main(String[] args){
Person person = new Girl();
person.introduce();
Person.introduce();
Girl.introduce();
}
}
- 结果是:
俺是一个人
俺是一个人
我是女孩,漂亮的女孩~
- 原因就不再重复了,上面说得hin清楚。
多态的使用场景
- 通过在某个方法的形参列表中,放入父类形参。调用方法时,传入子类实参,从而实现多态。
- 配合抽象类,实现模版设计模式。
抽象类
基本概念
- 抽象类不能实例化,类体中可以声明抽象方法。
- 抽象类的作用在于通过子类继承,并强制且规范地重写抽象方法,从而实现多态。
- 抽象类虽然不能实例化,但是可以写构造方法。写构造方法的意义是提供给子类进行调用。
abstract会和一些关键字闹矛盾哦!
- abstract关键字不能和下列关键字共同使用:
- private
- final
- static
- 原因都差不多,因为abstract的意义在于被继承和重写,而上述关键字会影响到类和方法的可继承性或可重新性。
继续用男孩女孩举栗子
public abstract class Person {
private int age;
Person(int age){
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public abstract void introduce();
}
public class Girl extends Person {
Girl(int age) {
super(age);
}
@Override
public void introduce() {
System.out.println("我是女孩,漂亮的女孩~芳龄:" + super.getAge());
}
}
public class Boy extends Person {
public Boy(int age){
super(age);
}
@Override
public void introduce(){
System.out.println("我是男孩,帅气的男孩!芳龄:" + super.getAge());
}
}
public class Test {
public static void main(String[] args){
Person person = new Girl(24);
person.introduce();
person = new Boy(23);
person.introduce();
}
}
- 运行结果如下。可以发现,abstract的作用在于规范和强制。抽象类中的成员变量和成员方法在于提取共性。抽象类很常用,但具体的思想只可意会,不可言传~百炼成钢。
我是女孩,漂亮的女孩~芳龄:24
我是男孩,帅气的男孩!芳龄:23
接口类
- 接口类的出现,是为了弥补单继承的缺陷。
接口类与抽象类的对比
- 接口类支持多实现,抽象类只能支持单继承。
- 接口类中不能有构造方法,抽象类中可以有构造方法。
- 接口类中只能有常量(final static),抽象类中可以有普通成员变量。
- 接口类只能有抽象方法,抽象类中可以有普通成员方法。
Java8之后产生的新特性
- Java8新特性:接口类支持default修饰的默认方法,实现类可以不用重写该方法,而直接使用该默认方法。
- Java8新特性:接口类支持静态方法。
- Java9新特性:接口中允许声明普通的私有方法,用于实现仅供默认方法调用的方法。
让男孩来展示一下接口类的方方面面
public interface Person {
//接口类中指定定义常量,并且注释中的几个关键字是可以省略的
/*public static final*/int LEG_COUNT = 2;
//接口类中只能有抽象方法,并且注释中的关键字可以省略
/*public abstract*/ void introduce();
}
public interface Animal {
//默认方法,实现类可以自行选择重写或不重写
/*public*/ default void eat(){
cry();
run();
System.out.println("一口吃掉苹果");
}
//私有方法,供默认方法调用
private void cry(){
System.out.println("嗷呜");
}
//静态方法,通过类名调用
/*public*/ static void run(){
System.out.println("跑啊跑,跑到外婆桥");
}
}
public class Boy implements Person, Animal {
@Override
public void introduce() {
System.out.println("我是男孩,帅气的男孩!俺有" + Person.LEG_COUNT + "条腿哦~");
}
}
public class Test {
public static void main(String[] args){
Boy boy = new Boy();
boy.introduce();
boy.eat();
Animal.run();
}
}
- 运行结果如下。这次讲解都放到注释里面了,所以就不在这里费口舌啦。
我是男孩,帅气的男孩!俺有2条腿哦~
嗷呜
跑啊跑,跑到外婆桥
一口吃掉苹果
跑啊跑,跑到外婆桥
特殊类
普通内部类
普通内部类的使用方式
- 和普通的类一样,可以定义成员变量、成员方法、构造方法等。其实就是除了位置比较特殊,其他的地方都和普通的类是一样的
- 同样可以使用final、abstract、private、public、protected等关键字修饰。
- 对于在外部类中的不重名成员变量,内部类可以直接访问。
- 普通内部类的实例化和重名成员变量的访问方式比较特殊,下面请看一段VCR(x)…请看一段代码:
public class Car {
private String color;
Car(String color){
this.color = color;
}
public class Wheel{
private String color;
public void paintWheel(String color){
this.color = color;
System.out.println("想要涂上的颜色是:" + color);
System.out.println("车*变成了:" + this.color);
System.out.println("车身子的颜色还是:" + Car.this.color);
}
}
}
public class Test {
public static void main(String[] args){
Car car = new Car("红色");
Car.Wheel carWheel = car.new Wheel();
carWheel.paintWheel("蓝色");
}
}
- 运行结果如下。我们可以发现,除了实例化内部类以及内部类重名成员变量的使用方法有点区别以外,其他的方面和普通的类没有太大区别。
- 内部类实例化方式:
外部类名.内部类名 内部类实例名 = 外部类实例名.new 内部类构造方法;
- 内部类重名变量使用方法:方法中的形参还是直接使用(因为就近原则)。内部类的变量使用
this.变量名
,外部类的重名变量使用外部类名.this.变量名
- 内部类实例化方式:
想要涂上的颜色是:蓝色
车*变成了:蓝色
车身子的颜色还是:红色
静态内部类
- 在声明方式上,和普通内部类的唯一区别就是在类名前加了个static关键字。使用时,实例化方法变成:
外部类名.内部类名 内部类实例名 = new 外部类名.内部类构造方法
- 静态内部类中,可以访问外部类中的静态成员变量和方法,但是不能访问非静态成员变量和方法。原因还是老规矩。
- 静态内部内中,访问外部内的静态成员和方法,使用
外部类名.
来访问;访问内部类的静态成员和方法 ,使用内部类名.
来访问。 - 如果非得访问外部类的非静态成员,也不是不行,可以在静态内部类的方法中实例化外部类,然后再通过引用来访问。
- 静态内部内中,访问外部内的静态成员和方法,使用
局部(方法)内部类
- 直接先上例子再来说明吧,不然容易晕乎。
public class Car {
private String color;
public void create(String color){
//Java8新特性:自动添加final关键字
/*final*/ int wheelCount = 4;
//局部内部类无需访问修饰符
class Wheel{
private String color;
public Wheel(){
System.out.println("创建*");
}
public void paint(String color){
this.color = color;
System.out.println(wheelCount + "个*涂成"+this.color);
}
}
this.color = color;
System.out.println("车子涂成"+this.color);
Wheel wheel = new Wheel();
wheel.paint("绿色");
}
}
public class Test {
public static void main(String[] args){
Car car = new Car();
car.create("红色");
}
}
- 执行结果如下。可以看到,局部内部类定义在外部类的成员方法中。
车子涂成红色
创建*
*涂成绿色
使用方法
- 局部内部类只在外部类的对应方法中可以使用。
- 局部内部类不能使用访问控制符和static关键字。
- 局部内部类可以访问外部类的对应方法中的局部变量,但该变量必须是final。
- 如上面例子中的wheelCount。因为wheelCount在内部类中被使用了,所以wheelCount必须是final修饰的。
- 原因是因为外部类的局部变量,被局部内部类使用时,局部内部类会将该变量拷贝一份(所以内外部类的该变量的地址是不同的)。为了防止发生类似“脏读”的现象,该变量必须是final的。
最常用的内部类——匿名内部类
- 当想要让重写的方法不要额外占用方法区内存空间的时候,可以使用匿名内部类。
- 使用普通的实现类或者子类来重写方法,需要一直占用方法区内存。而匿名内部类在方法执行完之后,会释放方法区的内存。
- 使用方式:
接口/父类类型 引用变量名 = new 接口/父类类型() { 方法的重写 };
。 - 举栗如下:
public interface Person {
void introduce();
}
public class Test {
public static void main(String[] args){
Person person = new Person() {
@Override
public void introduce() {
System.out.println("就是这么牛逼");
}
};
person.introduce();
}
}
- 执行结果如下。其实Java8之后增加了新特性——lambda表达式。所以上面这种写法以后就会逐渐淘汰啦~至于lambda表达式,就不在这篇文章里面讲了。
就是这么牛逼
枚举类
基本用法
- 其实在Java1.5之前,枚举类都是大家自己写的。写法和单例模式很像,单例模式只提供一个成员实例,而枚举类提供多个不同类的成员实例。
- 那么Java1.5之后产生的枚举类,是怎么样的呢?直接上代码!
public enum Week {
//枚举类型要求所有枚举值放在类的最前面
MON("周一"),TUE("周二"),THUR("周三"),SAT("周四");
private final String desc;
Week(String desc) {
this.desc = desc;
}
public String getDesc(){
return desc;
}
}
public class Test {
public static void main(String[] args) {
Week tue = Week.TUE;
System.out.println(Week.MON.getDesc());
System.out.println(tue.getDesc());
}
}
- 运行结果如下:
周一
周二
- 其实,枚举类型就是语法省略后的本类对象的引用。例如
MON("周一");
,其实代表的是public static final Week MON = new Week("周一");
- 使用方法也是完全和访问静态成员变量的方法一样。
- 枚举类可以自定义构造方法,但是前提必须得是private修饰的。
- 枚举类型最好使的地方就是配合switch,具体就不多说了。懂的自然懂~
常用方法
- 所有枚举类型都默认继承自java.lang.Enum类,这个类中有一些方法可供大家使用:
方法名 | 说明 |
---|---|
static T[] values() | 返回当前枚举类中所有的对象 |
String toString() | 返回当前枚举类对象的标识名称 |
int ordinal() | 获取枚举对象在枚举类中的索引位置 |
static T valueOf(String str) | 将参数指定的字符串名转为对应的枚举类对象。如果对应的枚举类型不存在,则抛出非法参数异常 |
int compareTo(E o) | 比较两个枚举对象在定义时的顺序。<0:调用该方法的枚举类型在实参枚举类型的前面;=0:相同枚举类型;>0:调用该方法的枚举类型在实参枚举类型的后面 |
- 续上面的例子,来感受感受这几个方法:
//Week类不变,Test中的main方法增加几行代码
public class Test {
public static void main(String[] args) {
Week tue = Week.TUE;
System.out.println(Week.MON.getDesc());
System.out.println(tue.getDesc());
Week[] weeks = Week.values();
System.out.println(weeks[3].toString() + "的索引是:" + weeks[1].ordinal());
Week thur = Week.valueOf("THUR");
System.out.println(thur + "和" + Week.SAT.toString() + "比较的结果是:" + thur.compareTo(Week.SAT));
}
}
- 运行结果如下:
周一
周二
SAT的索引是:1
THUR和SAT比较的结果是:-1
枚举类实现接口的方式
- 第一种方式和以前一样,在枚举类(例如Week类)中重写方法。此时所有枚举类型都会调用该方法。这种就不举例了,没有区别。
- 第二种是让每一个枚举类都单独重写方法。通过匿名内部类即可实现该需求。具体说来是什么样子呢?请看下面:
//增加一个接口类
public interface WeekInterface {
void workday();
}
//相较于上一个例子,实现了一个接口类,并且使用匿名内部类重写了接口方法
public enum Week implements WeekInterface {
//枚举类型要求所有枚举值放在类的最前面
MON("周一"){
@Override
public void workday(){
System.out.println("周一是工作日呀!");
}
},TUE("周二"){
@Override
public void workday(){
System.out.println("周二还是工作日呀!");
}
},THUR("周三"){
@Override
public void workday(){
System.out.println("周三怎么还是工作日呀!");
}
},SAT("周四"){
@Override
public void workday(){
System.out.println("周四还是工作日!好气哦!");
}
};
private final String desc;
Week(String desc) {
this.desc = desc;
}
public String getDesc(){
return desc;
}
}
//测试类增加一点代码
public class Test {
public static void main(String[] args) {
Week tue = Week.TUE;
System.out.println(Week.MON.getDesc());
System.out.println(tue.getDesc());
Week[] weeks = Week.values();
System.out.println(weeks[3].toString() + "的索引是:" + weeks[1].ordinal());
Week thur = Week.valueOf("THUR");
System.out.println(thur + "和" + Week.SAT.toString() + "比较的结果是:" + thur.compareTo(Week.SAT));
System.out.println("------------------------------------------------------------------------");
WeekInterface weekInterface = Week.MON;
weekInterface.workday();
Week.SAT.workday();
}
}
- 执行结果如下:
周一
周二
SAT的索引是:1
THUR和SAT比较的结果是:-1
------------------------------------------------------------------------
周一是工作日呀!
周四还是工作日!好气哦!
- 可能枚举类的匿名内部类的编写方法看起来有点难以接受。其实很好理解,因为枚举类型本来就是简写的过程。拿周一举个例子,通过下面的对比,大家应该就能明白了:
//标准的匿名内部类
public static final Week MON = new Week("周一"){
@Override
public void workday(){
System.out.println("周一是工作日呀!");
}
};
//枚举类中简化的匿名内部类
MON("周一"){
@Override
public void workday(){
System.out.println("周一是工作日呀!");
}
};
注解
- 注解这个玩意儿大家可能经常用到,但是应该不是每个人都去了解过。他其实也是一种类,一种“特殊的接口类”。
- 注解通常能够修饰包、类、 成员方法、成员变量、构造方法、参数、局部变量的声明。
- 所有注解都自动继承了一个接口类——Annotation类。
- 注解类的声明语法如下:
访问修饰符 @interface 注解名称 {
注解成员;
}
- 注解这东西基本概念比较简单,但是今后在各类框架、多线程等开发中可是起着举足轻重的作用!
注解类的声明方式
public @interface Goods {
//这个其实是一个名字为type的String类型成员变量。但是是通过“无形参方法”的形式声明的
/*public*/ String type();
double price() default 1.0;
}
@Goods(type = "food", price = 1.1)
public class Food {
}
- 这个例子就没有运行结果了。其中需要提一下的是,注解类中看起来很像声明成员方法的代码,其实声明的是成员变量。
元注解
- 元注解是用在注解类上的注解。
- 五种常见元注解:@Retention、@Documented、@Target、@Inherited、@Repeatable。
@Retention
- 用于描述注解的生命周期,有一个成员变量value,取值如下:
- RetentionPolicy.SOURCE:注解仅在源码阶段保留,编译时将会忽略。
- RetentionPolicy.CLASS:被保留到编译后的字节码文件,不会加载进JVM。默认方式。
- RetentionPolicy.RUNTIME:保留到程序运行阶段,加载进JVM,可以在运行阶段通过反射机制获取到注解。
@Documented
- JavaDoc工具默认不会包括注解内容。如果想让JavaDoc文档说明某个注解类的使用地方,则用@Documented来注解该注解类。
- 如果要让这个注解生效,必须让@Retention的值为RetentionPolicy.RUNTIME。
@Target
- 该注解用于限制某个注解类能够使用的地方。该注解有一个成员变量,类型为ElementType[],具体取值如下:
- ElementType.ANNOTATION_TYPE:可以对注解类进行注解
- ElementType.CONSTRUCTOR:可以对构造方法进行注解
- ElementType.FIELD:可以对属性(成员变变量)进行注解
- ElementType.LOCAL_VARIABLE:可以对局部变量进行注解
- ElementType.METHOD:可以对方法进行注解
- ElementType.PACKAGE:可以对包进行注解
- ElementType.PARAMETER:可以对方法形参进行注解
- ElementType.TYPE:可以对类进行注解,例如类、接口、枚举
- Java8新增:ElementType.TYPE_PARAMETER:可对类型变量的声明(如泛型)进行注解
- Java8新增:中ElementType.TYPE_USE:可对使用类型的任何语句进行注解
- 需要注意的是,这个注解使用的时候,需要指定的值是一个数组,所以需要用数组的方式,来放入N个值。
@Inherited
- 该注解注解了某个注解类之后,如果该注解类使用在了一个超类上,且该超类的某个子类没有任何注解,则子类自动继承超类的注解。
Java8新增元注解:@Repeatable
- 在不加该注解的情况下,同一个注解只能对一个元素注解一次。但是在使用了该注解之后,即可多次使用同一注解。
- 这个注解的使用方式可能比较难理解,下面举个例子:
//这个类使用@Repeatable注解,该注解需要通过反射机制传入GoodsTypes注解类
@Repeatable(GoodsTypes.class)
public @interface Goods {
//这个其实是一个名字为type的String类型成员变量。但是是通过“无形参方法”的形式声明的
/*public*/ String type();
double price() default 1.0;
}
//该注解类需要一个Goods数组类型的成员变量,并且标识名称必须为value
public @interface GoodsTypes {
Goods[] value();
}
//使用注解之后,@Goods就可以多次使用了。其实可以理解为@Repeatable注解就是简化了下面注释掉的@GoodsTypesj'j'n'b
@Goods(type = "food", price = 1.1)
@Goods(type = "vehicle", price = 11)
//@GoodsTypes({@Goods(type = "food", price = 1.1), @Goods(type = "vehicle", price = 11)})
public class Food {
}
Java的一些常见预制注解
- 文档注释用的:
- @author、@version、@see、@since、@param、@return、@exception
- @Override:限定重写方法
- @Deprecated:表示方法或者类已过时
- @SuppressWarnings:抑制编译器警告
包装类
自动拆箱机制
- 猜猜下面的结果是啥
- 结果如下:
true
false
- 为什么呢?
- 对于第一个比较:大家可能第一时间会想到==比较的是地址,所以理应为false。但是包装类在进行运算符运算的时候,会触发“自动拆箱机制”,也就是说
a + b
这东西,计算结果会从Integer变为int。而Long与int进行比较,Long也会变为long。所以最终就变成了基本数据类型的比较,结果自然是true。 - 对于第二个比较:这个比较简单,因为Long类重写了equals方法,自己去看一下就知道了。如果比较的是非Long类型,则直接为false。
- 对于第一个比较:大家可能第一时间会想到==比较的是地址,所以理应为false。但是包装类在进行运算符运算的时候,会触发“自动拆箱机制”,也就是说
常量池
- 猜猜下面的结果是啥
- 结果是
true
- 有的小伙伴又懵了:怎么着?这次还不比地址么?
- 不,这次真的比的是地址,但却是就是同一个地址。为什么呢?因为包装类有一个叫常量池的东西,对于-128~127的整数,对应的包装类都作为常量存放在了内存当中。所以a和b其实都是直接指向的内存中的这些常量,地址也就理所当然是相同的了。
- 同理,如果将a和b改为128,再进行测试,结果就是
false
。