尽管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中方法默认是动态绑定的,除了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的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
注意, null 可以匹配任何类对象。重载的调用是编译时行为,只看编译时类型
规则:
推广:
如果变量是基本数据类型,此时赋值的是变量所保存的数据值。
如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值。
ps:
因为java中方法调用传入的是实参的值而不是引用(址),所以不能改变实参的值,即基本数据类型的数据值和引用类型的地址值,但引用对象本身可变,除了不可变对象.(这与final修饰属性相同)(与js一样)
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,任何对它的改变都应该产生一个新的对象,对string来说即地址和字面量一一绑定。
不可变对象的类即为不可变类(Immutable Class)。JAVA平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。因此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"三个空间,没有在堆(非常量池)中产生对象.
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理解为:当前对象 或 当前正在创建的对象
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 { 编译不过
protected static final ThreadLocal> LOCAL_PAGE = new ThreadLocal>(); */
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,可以在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规定)
(不会发生类的初始化)
默认初始化值(0,false,null)
类初始化:加载-链接-初始化:
#创建对象时进行非静态属性的初始化
1.即先赋值后声明
2.未显示初始化的属性只能以方法等非简单形式引用(编译不报错但引用对象不稳定不安全应避免)
原理:默认初始化与显示初始化的分离,加载class时先依次将字段添加到符号表并默认初始化,此时向前引用该变量也只有默认初始化值,
3.非法的向前引用,编译不过
(未声明就以简单名称的形式在代码块语句"="右边或其他成员的初始化语句中)
前两步为加载类,静态属性初始化(默认初始化,显示初始化/代码块)
1.主类父类的静态代码 —出现顺序
2.主类的静态代码 —出现顺序
3.main函数
以下为main方法中new一个类型时的顺序:
#实例父类的静态代码 —出现顺序
#实例子类的静态代码 —出现顺序
非静态属性初始化
4.默认初始化
5.父类非静态代码 —出现顺序
6.父类构造器
7.子类非静态代码 —出现顺序
8.子类构造器
ps:public static 主类 变量名 = new 主类();静态语句创建本类对象,会跳过剩余的静态语句,调用非静态语句实例化完后,再走剩余的静态语句。递归的初始化尝试会直接被忽略掉[JLS 12.4.2, 第3 步];但是在非静态语句中创建本类或父类的对象会导致被调用时一直循环创建对象,导致死循环。
对于第二点,spring循环依赖注入在set注入情况下,使用三级缓存进行了解决.
>全局常量:public static final的.但是书写时,一般省略不写(必须初始化,但不要求是常量表达式)
ps:一般不在接口内定义一堆常量,多继承接口时,如果有相同名常量,不能通过简单名进行引用(会报错,即使两者值一样),言外之意可以通过接口名限定来分别正常引用.
>抽象方法:public abstract的,一般省略不写
接口内的类/接口自动成为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.成员内部类:
//创建静态的Dog内部类的实例(静态的成员内部类):
Person.Dog dog = new Person.Dog();
//创建非静态的Bird内部类的实例(非静态的成员内部类):
//Person.Bird bird = new Person.Bird();//错误的
Person p = new Person();
Person.Bird bird = p.new Bird();
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();
}
}
}
//返回一个实现了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的。
总结:
成员内部类和局部内部类,在编译以后,都会生成字节码文件。
格式:成员内部类:外部类 内 部 类 名 . c l a s s 局 部 内 部 类 : 外 部 类 内部类名.class 局部内部类:外部类 内部类名.class局部内部类:外部类数字 内部类名.class
匿名内部类:外部类 数 字 . c l a s s 在 方 法 中 n e w 一 个 成 员 内 部 类 对 象 编 译 时 也 会 生 成 外 部 类 数字.class 在方法中new一个成员内部类对象 编译时也会生成外部类 数字.class在方法中new一个成员内部类对象编译时也会生成外部类数字.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");
}
}
异常的两个特点(遇到过类似面试题)