javaSE语法踩坑(二) -深入理解java的面向对象

tech2023-02-27  96

面向对象的关键字

关于数组属性与变量方法多态重载JVM在重载方法中,择合适的目标方法的顺序如下(越精确越优先): 可变形参参数传递String常量池intern()方法包装类常量池 this/super与继承继承与修饰符的关系: 静态与类/实例初始化静态成员不能引用类的泛型变量 类的主动引用时发生类初始化类的被动引用初始化过程Java初始化时可以向前引用:例子:JAVA主类中语句执行顺序 接口与内部类接口内部类: 异常一个异常和泛型的有趣内容 Lombok注解-@SneakyThrows 在上一篇语法踩坑之后,终于有空继续总结我遇到的冷门易错语法,可以解决我们遇到的绝大部分问题。如果还有什么特殊情况,可以参阅《Java Puzzlers》,《Java语言规范 基于 Java SE 8》官方JLS,《深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版)》三本书。

关于数组

尽管char 是一个整数类型,但是许多类库都对其进行了特殊处理,因为char数值通常表示的是字符而不是整数。例如,将一个char 数值传递给println 方 法会打印出一个Unicode 字符而不是它的数字代码。字符数组受到了相同的特殊处理:println 的char[]重载版本会打印出数组所包含的所字符,而String.valueOf和StringBuffer.append的char[]重载版本的行为也是类似的。 其他数组只有使用println(Obj),会调用Obj.toString

引用类型数组可以向上转型,但非常不安全.但添加超类型引用的其他子类元素在运行时会导致ArrayStoreException。

从反射的角度再理解数组: 所有具有相同元素类型和维数的数组都共享该Class对象。 由于数组没构造函数,我们也就没办法通过它的Class对象直接创建数组对象。为了实现这个功能,JDK中就提供了Array类(java.lang.reflect.Array)来弥补这个缺陷。

属性与变量

方式一:按照数据类型:

方式二:按照在类中声明的位置:

ps:局部变量作用域限定在{}内,代码块局部变量不可嵌套(不可与代码块外部的变量重名),但方法局部变量可以嵌套并遮蔽掉成员变量.

方法

ps:java中方法不是一级结构,不能作为对象或数据被传递,只能依附于类等一级结构而存在.因此方法内不能定义方法.由于Java中没函数指针、仿函数、委托这样的概念, 因此要将一个算法传入一个方法中唯一的选择就是通过接口回调,既通过函数式接口,lambda表达式等来实现函数式编程与闭包.

思考:方法的调用者其实可算作方法的一个隐式参数,java的方法必须有调用者 这个参数对成员方法来说是调用对象,对静态方法则是这个调用对象的类对象. 这样方法中才能使用this.,super.来使用类的以及父类的所有成员,调用静态成员时为避免混淆应使用类名. 静态方法中只能使用类名,因此只能调用静态成员. 另外如果没有出现隐藏或重写等名字重用情况时,这三种限定符都应该省去.

关于重写,在上一篇 总结(一)中有详述

多态

java中方法默认是动态绑定的,除了final,static,private及构造方法是静态绑定(非虚拟方法).

有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的是子类重写父类的方法。 总结:编译,看左边;运行,看右边。 ps:静态绑定属于编译级别,在声明引用类型时就已确定,(如这样的强转转换 变量1=(子类型)变量1 不会影响被转换的变量类型) 结果就是 一个对象的不同类型的变量引用调用非静态方法结果总是一致的,调用静态方法或者属性则结果与变量类型有关

ps:构造器中应避免多态:

尽量不调用除final和private之外的其他方法以避免子类重写造成难于理解,【例如父类构造器中的方法,创建子类对象时this指代当前子类对象形成多态.此时如果子类重写方法使用了子类的成员变量就会读取到默认值(0,null等),参见后文属性初始化顺序介绍】. 千万不要在构造器中调用可覆写的方法,直接调用或间接调用都不行[EJ Item 15]。这项禁令应该扩展至实例初始器和伪构造器(pseudoconstructors)readObject 与clone。(这些方法之所以被称为伪构造器,是因为它们可以在不调用构造器的情况下创建对象。) 不要在构造器中调用可覆 写的方法。在实例初始化中产生的循环将是致命的。该问题的解决方案就是惰性初始化[EJ Items 13,48]。

重载

“两同一不同”:同一个类、相同方法名 参数列表不同:参数个数不同,参数类型不同。 跟方法的权限修饰符、返回值类型、形参变量名、方法体都没关系! Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。 所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;而对于多态,只等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。

引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”

JVM在重载方法中,择合适的目标方法的顺序如下(越精确越优先):

1,精确匹配;2,基本数据类型自动转换;3,自动拆箱与装箱;4,依次向上转型;(基本类型数组不能装箱为包装类型数组,会直接转为Object)5,可变参数

注意, null 可以匹配任何类对象。重载的调用是编译时行为,只看编译时类型

可变形参

2.1 可变个数形参的格式:数据类型 … 变量名2.2 当调用可变个数形参的方法时,传入的参数个数可以是:0个,1个,2个,。。。2.3 可变个数形参的方法与本类中方法名相同,形参不同的方法之间构成重载2.4 可变个数形参的方法与本类中方法名相同,形参类型也相同的数组之间不构成重载。换句话说,二者不能共存。2.5 可变个数形参在方法的形参中,必须声明在末尾2.6 可变个数形参在方法的形参中,最多只能声明一个可变形参。

参数传递

规则:

如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值。如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值。

推广: 如果变量是基本数据类型,此时赋值的是变量所保存的数据值。 如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值。

ps: 因为java中方法调用传入的是实参的值而不是引用(址),所以不能改变实参的值,即基本数据类型的数据值和引用类型的地址值,但引用对象本身可变,除了不可变对象.(这与final修饰属性相同)(与js一样) 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,任何对它的改变都应该产生一个新的对象,对string来说即地址和字面量一一绑定。 不可变对象的类即为不可变类(Immutable Class)。JAVA平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。因此String作为参数传入原实参不会变化,(对形参进行常量表达式赋值实际是赋值了新对象的地址,原实参仍然持有原地址值). 只能通过"=“赋新值来改变,与”."操作对象相区别,变化不具有传递性.

public static void main(String[] args) { Integer a = new Integer(2); Integer b = a; a = 1; System.out.println(b); //结果为2 }

String常量池

上面提到了string’的不可变性,再说说java对String进行的字符串常量池优化 https://www.cnblogs.com/duanxz/p/3613947.html

String str1 = "abc"; String str2 = "abc"; 常量池的存在使 str1 == str2 永远为true;以上只在常量字符串常量池中占用了一个"abc"空间 String str6 = "ab"; String str7 = "c"; String str8 = str3 + str4; str8 == str1永远为false;以上只有str8在堆(非常量池)中产生对象,"ab""c"在字符串常量池中. final String str3 = "ab"; final String str4 = "c"; String str5 = str3 + str4; 常量表达式的编译器优化 使 str5 == str1永远为true;以上三行代码只在常量字符串常量池中占用了"ab" ,"c","abc"三个空间,没有在堆(非常量池)中产生对象.

intern()方法

String.intern() 是一个 Native 方法,它的作用(在JDK1.6和1.7操作不同)是:如果字符串常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,在jdk1.6中,将此String对象添加到常量池中,然后返回这个String对象的引用(此时引用的串在常量池)。在jdk1.7中,放入一个引用,指向堆中的String对象的地址,返回这个引用地址(此时引用的串在堆)。

也就是 str.intern() == "与str相同的字面量表达式" 永远是true,但str == str.intern()却不一定.

看《深入理解java虚拟机》中的一个例子 《深入理解java虚拟机》中写道,如果JDK1.6及之前会返回两个false,JDK1.7及以后运行则会返回一个true一个false。

JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串的实例的引用,而StringBulder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。

JDK1.7中,intern()的实现不会在复制实例,只是在常量池中记录首次出现的实例引用,因此返回的是引用和由StringBuilder.toString()创建的那个字符串实例是同一个。

str2的比较返回false因为"java"这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串是首次出现,因此返回true。(System类自动由java虚拟机调用, 其中把"java"加入到了常量池中)

关于常量池的更多内容参见https://blog.csdn.net/q5706503/article/details/84640762

包装类常量池

另外在JDK1.5之后,基本类型有了包装类,其大部分都用程序实现了类似于常量池技术,即 Byte、Short、Integer、Long、Character、Boolean;这5种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 两种浮点数类型的包装类 Float、Double 并没有实现常量池技术。

public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } //其中IntegerCache 为Integer的私有静态内部类.其他数字包装类类似处理 private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; ... } //CharacterCache 的范围为0-127 private static class CharacterCache { private CharacterCache(){} static final Character cache[] = new Character[127 + 1]; static { for (int i = 0; i < cache.length; i++) cache[i] = new Character((char)i); } }

this/super与继承

this理解为:当前对象 或 当前正在创建的对象 ps:(准确的说是对调用当前方法的对象的引用,该对象类型为定义时的上下文与变量类型无关,如当出现子类*隐藏(hide)*父类属性时,(this.)属性时就指定是当前类型了。)

super 关键字可以理解为:父类的 ps:(与this不同,super不是一个对象的引用,不能将super赋给另一个引用类型的变量,可看作一个修饰符)

继承与修饰符的关系:

继承属性,子类可以随意改写隐藏,不受修饰符及数据类型影响.(内部类同理) abstract,不影响,父类可以被子类重新抽象化,子类可以实现父类,方法也是并且符合@Override. static,子类可以继承父类静态方法,也可以声明与父类相同方法签名的静态方法,但仍需遵守重写规则,此时会隐藏掉父类方法,不符合@Override. 非静态方法重写必须为非静态. synchronized,不被继承,既子类重写父类同步方法时可以选择是否用synchronized修饰以达到同步效果.子类也可将父类非同步方法重写为同步. final,父类final方法不能被重写或隐藏(静态),子类可以将父类非final方法改写为final.

静态与类/实例初始化

静态成员不能引用类的泛型变量

//mybatis插件PageHelper使用了ThreadLocal静态属性来储存结果,它不能通过引用PageMethod泛型参数来指定Page的泛型 /**public abstract class PageMethod <T>{ 编译不过 protected static final ThreadLocal<Page<T>> LOCAL_PAGE = new ThreadLocal<Page<T>>(); */ public abstract class PageMethod { //Page使用原始类型 protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>(); protected static boolean DEFAULT_COUNT = true; /** * 设置 Page 参数 * @param page */ protected static void setLocalPage(Page page) { LOCAL_PAGE.set(page); } ... /** * 开始分页 * * @param pageNum 页码 * @param pageSize 每页显示数量 * @param count 是否进行count查询 * @param reasonable 分页合理化,null时用默认配置 * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置 */ public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page<E> page = new Page<E>(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); //当已经执行过orderBy的时候 Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } setLocalPage(page); return page; } ... } //Page类的泛型继承自ArrayList<E>,可以在startPage时指定,但不会影响ThreadLocal public class Page<E> extends ArrayList<E> implements Closeable { ... } //导致诸如以下代码能绕过泛型正常编译,却会在运行时报错 Page<想要的类型> pageInfo= PageHelper.startPage(1,2); List<实际类型> result = xxDAO.listBy(param);//执行select的sql PageInfo<实际类型> of = PageInfo.of(result); PageInfo<想要的类型> ofPage = PageInfo.of(pageInfo); ///List<想要的类型> list = ofPage.getList(); 运行期报错 List list = ofPage.getList();//可以通过原始类型来消除泛型限制 List<实际类型> list1 = (List<实际类型>) list;

类的主动引用时发生类初始化

(一定会发生类的初始化,jVM规定)

当虚拟机启动,先初始化main方法所在的类new一个类的对象调用类的静态成员(除了final常量,已在编译阶段把结果放入常量池)和静态方法使用java.lang.reflect包的方法对类进行反射调用当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类 

类的被动引用

(不会发生类的初始化)

 当访问一个静态域时,只有真正声明这个域的类才会被初始化 当通过子类引用父类的静态变量,不会导致子类初始化 通过数组定义类引用,不会触发此类的初始化 引用常量不会触发此类的初始化

初始化过程

默认初始化值(0,false,null)

类初始化:加载-链接-初始化:

#链接时会进行为静态变量分配内存空间也就是默认初始化,静态常量在其初始化表达式是常量表达式时会直接初始化.#初始化类时,对静态变量进行初始化-------ps:静态内部类在调用时才会初始化,利用这点可以用静态内部类实现线程安全的延迟加载单例模式.显式初始化/在代码块中赋值(按类中的初始化代码顺序)

#创建对象时进行非静态属性的初始化

①默认初始化②显式初始化/⑤在代码块中赋值(按类中的初始化顺序)③构造器中初始化④有了对象以后,可以通过"对象.属性"或"对象.方法"的方式,进行赋值初始化的先后顺序:① - ② / ⑤ - ③ - ④

Java初始化时可以向前引用:

1.即先赋值后声明 2.未显示初始化的属性只能以方法等非简单形式引用(编译不报错但引用对象不稳定不安全应避免) 原理:默认初始化与显示初始化的分离,加载class时先依次将字段添加到符号表并默认初始化,此时向前引用该变量也只有默认初始化值, 3.非法的向前引用,编译不过 (未声明就以简单名称的形式在代码块语句"="右边或其他成员的初始化语句中)

例子:JAVA主类中语句执行顺序

前两步为加载类,静态属性初始化(默认初始化,显示初始化/代码块) 1.主类父类的静态代码 —出现顺序 2.主类的静态代码 —出现顺序 3.main函数 以下为main方法中new一个类型时的顺序: #实例父类的静态代码 —出现顺序 #实例子类的静态代码 —出现顺序 非静态属性初始化 4.默认初始化 5.父类非静态代码 —出现顺序 6.父类构造器 7.子类非静态代码 —出现顺序 8.子类构造器 ps:public static 主类 变量名 = new 主类();静态语句创建本类对象,会跳过剩余的静态语句,调用非静态语句实例化完后,再走剩余的静态语句。递归的初始化尝试会直接被忽略掉[JLS 12.4.2, 第3 步];但是在非静态语句中创建本类或父类的对象会导致被调用时一直循环创建对象,导致死循环。 对于第二点,spring循环依赖注入在set注入情况下,使用三级缓存进行了解决.

接口与内部类

接口

1.接口使用interface来定义2.Java中,接口和类是并列的两个结构(和enum一样只能被public或缺省修饰)3.如何定义接口:定义接口中的成员3.1 JDK7及以前:只能定义全局常量和抽象方法 >全局常量:public static final的.但是书写时,一般省略不写(必须初始化,但不要求是常量表达式) ps:一般不在接口内定义一堆常量,多继承接口时,如果有相同名常量,不能通过简单名进行引用(会报错,即使两者值一样),言外之意可以通过接口名限定来分别正常引用. >抽象方法:public abstract的,一般省略不写 3.2 JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法(必须用default显式修饰,可被继承,和父类方法体冲突时,父类优先) 注意:接口静态方法不被继承!!!default不是权限修饰符,接口方法仍然默认public,多实现/继承接口时,同签名方法只要有一个默认实现就会报错,解决办法可以重写该方法或分别定义内部类来实现接口… 扩展 3.3 JDK9 : 私有方法3.4 接口的嵌套:可以在class和接口内定义接口

接口内的类/接口自动成为public static,实现外部接口不用实现内部接口.

类的内部接口可以private,即对外不可见,只能在该类中实现,可以实现为public类,但仍然只能被该外部类成员使用. 类的内部接口自动成为static(只有这样才能使用,才有意义).

PS:接口没有继承Object,但是隐式的声明了许多与Object相同的public abstract方法(不具有实际意义,也可显示声明但需按重写规则与Object一致,最终实现类总是优先继承Object的方法,因此也没有什么意义),并且object是接口的超类型,所以可以向上转型为Obj.

内部类:

类的第五个成员(内部类可以不受外部类影响来继承类或实现接口) 1.定义:Java中允许将一个类A声明在另一个类B中,则类A就是内部类,类B称为外部类. 2.内部类的分类: 成员内部类(静态、非静态 ) vs 局部内部类(方法内、代码块内、构造器内)(又叫本地类) 3.成员内部类的理解: 一方面,作为外部类的成员:

>调用外部类的结构 >可以被static修饰(普通内部类隐式的保存了一个引用,指向创建它的外围类对象,静态内部类又叫嵌套类则没有) 静态内部类没有外部类对象引用,因此只能访问外部静态属性和方法,可以有静态属性和方法,静态代码块以及静态内部类和接口(内部接口默认隐式静态). 非静态内部类不能有静态成员(除了常量变量,因为static是和类相关,但非静态内部类是依赖于外部实例存在). 注意外部类中可以通过内部类实例访问内部类成员,包括私有成员,内部类则可以直接访问外部类私有成员.在顶层的类型(top-level type)中,所有的本地的、内部的、嵌套的和匿名的类都可以毫无限制地访问彼此的成员[JLS 6.6.1]。。包括私有成员,这是一个欢乐的大家庭。但私有成员不会被继承[JLS 8.2]。 >可以被4种不同的权限修饰

另一方面,作为一个类:

> 类内可以定义属性、方法、构造器等 > 可以被final修饰,表示此类不能被继承。言外之意,不使用final,就可以被继承 > 可以被abstract修饰

4.成员内部类:

4.1如何创建成员内部类的对象?(静态的,非静态的) //创建静态的Dog内部类的实例(静态的成员内部类): Person.Dog dog = new Person.Dog(); //创建非静态的Bird内部类的实例(非静态的成员内部类): //Person.Bird bird = new Person.Bird();//错误的 Person p = new Person(); Person.Bird bird = p.new Bird(); 4.2如何在成员内部类中调用外部类的结构? class Person{ String name = "小明"; public void eat(){ } //非静态成员内部类 class Bird{ String name = "杜鹃"; public void display(String name){ System.out.println(name);//方法的形参 System.out.println(this.name);//内部类的属性 System.out.println(Person.this.name);//外部类的属性 //Person.this.eat(); } } } 5.局部内部类的使用:(局部类能完全对外隐藏,作用域被限定,且可以访问该方法的局部变量(final),同局部变量一样不能有权限及static修饰符,其他规则等同非静态成员内部类) //返回一个实现了Comparable接口的类的对象 public Comparable getComparable(){ //创建一个实现了Comparable接口的类:局部内部类 //方式一: // class MyComparable implements Comparable{ // // @Override // public int compareTo(Object o) { // return 0; // } // // } // // return new MyComparable(); //方式二: return new Comparable(){ @Override public int compareTo(Object o) { return 0; } }; }

注意点: 在局部内部类的方法中(比如:show如果调用局部内部类所声明的方法(比如:method)中的局部变量(比如:num)的话,要求此局部变量声明为final的。

(闭包) jdk 7及之前版本:要求此局部变量显式的声明为final的 jdk 8及之后的版本:可以省略final的声明,这样可以直接使用方法的参数…

总结: 成员内部类和局部内部类,在编译以后,都会生成字节码文件。 格式:成员内部类:外部类 内 部 类 名 . c l a s s 局 部 内 部 类 : 外 部 类 内部类名.class 局部内部类:外部类 .class数字 内部类名.class 匿名内部类:外部类 数 字 . c l a s s 在 方 法 中 n e w 一 个 成 员 内 部 类 对 象 编 译 时 也 会 生 成 外 部 类 数字.class 在方法中new一个成员内部类对象 编译时也会生成外部类 .classnew数字.class

匿名内部类的语法: new 父类构造器(实参列表)|| 实现接口() { //匿名内部类的类体部分 } 匿名子类对象.lambda表达式

ps:内部类可以按照属性来对比理解,父类的内部类可以被子类继承,但是没有多态,子类可以重新声明一个相同的内部类来隐藏父类的内部类. 内部类特殊语法: .this; .new ; 外部类对象.super(内部类被继承时,子类使用该内部类的成员的语法).

ps:一个包内私有的方法不能被位于另一个包中的某个方法直接覆写(重写)[JLS 8.4.8]。

package hack; import click.CodeTalk; public class TypeIt { private static class ClickIt extends CodeTalk { //这里不算重写,不能用@Override void printMessage() { System.out.println("Hack"); } } public static void main(String[ ] args) { ClickIt clickit = new ClickIt(); clickit.doIt(); //打印 Click } } package click; public class CodeTalk { public void doIt() { printMessage(); } void printMessage() { System.out.println("Click"); } }

异常

异常的两个特点(遇到过类似面试题)

catch中匹配时会自动向下转型窄化匹配,类似于用异常对象来instanceof catch的类型;finally不会影响try或者catch中的return值,但如果finally中有了return/异常等会覆盖try和catch中的异常和return.ps:如果一个catch 子句要捕获一个类型为E 的被检查异常,而其相对应的try 子句不能抛出E 的某种子类型的异常,那么这就是一个编译期错误[JLS 11.2.3]。Java 不允许静态初始化操作抛出被检查异常,非静态初始化抛出的异常会传给构造方法.如果初始化操作抛出的是被检查异常,那么构造器必须声明也会抛出这些异常,但是应该避免这样做,因为它会造成混乱.

一个异常和泛型的有趣内容 Lombok注解-@SneakyThrows

lombok源码:

public static RuntimeException sneakyThrow(Throwable t) { if (t == null) throw new NullPointerException("t"); return Lombok.<RuntimeException>sneakyThrow0(t); } @SuppressWarnings("unchecked") private static <T extends Throwable> T sneakyThrow0(Throwable t) throws T { throw (T)t; }

使用代码

import lombok.SneakyThrows; public class SneakyThrowsExample implements Runnable { @SneakyThrows(UnsupportedEncodingException.class) public String utf8ToString(byte[] bytes) { return new String(bytes, "UTF-8"); } @SneakyThrows public void run() { throw new Throwable(); } }

编译后

import lombok.Lombok; public class SneakyThrowsExample implements Runnable { public String utf8ToString(byte[] bytes) { try { return new String(bytes, "UTF-8"); } catch (UnsupportedEncodingException e) { throw Lombok.sneakyThrow(e); } } public void run() { try { throw new Throwable(); } catch (Throwable t) { throw Lombok.sneakyThrow(t); } } }
最新回复(0)