目录
Java语言特性
1、Java语言的特点(7大特点):
- 面向对象
- 平台无关性(JVM)
- 支持多线程
- 安全性: java通过避免采用显示的指针以减少安全攻击和风险。因为,指针存储了另一个值的内存地址,而这个值可能导致内存被未经授权访问。而java的安全管理器可以为每一个应用定义存取规则。
- 支持网络编程且方便
- 编译与解释并存
2、JVM、JDK、JRE
- JVM(Java Virtual Machine)是运行Java字节码的虚拟机。JVM有针对不同系统的特定实现,目的是使用相同的字节码都能得到相同的结果。
- JRE(Java Runtime Enviroment)是运行已编译Java程序所需的所有内容的集合,包括JVM,Java类库,Java命令和其他一些基本构建,但不能用于创建新程序。
- JDK(Java Development Kit)是功能齐全的Java SDK,拥有JRE的一切,还有编译器(Javac)和工具(Javadoc和Jdb),它能够创建和编译程序。
3、Java为什么是解释与编译并存的语言?
- Java需利用Javac将源代码编译成.class的字节码文件,然后交给Java解释器来解释执行。
- 解释器的执行速度相对比较慢,后来引入了JIT编译器,属于运行时编译,将一些经常被调用的方法和代码块完成第一次编译后,将字节码对应的机器码保存起来,下次可以直接使用,机器码的运行效率要高于解释器。
注:说到编译器,最后有一个AOT属于运行前编译,是直接在程序执行前生成Java方法的本地代码,以便于在程序执行时直接使用本地代码。在编译速度上:解释执行>AOT编译器>JIT编译器;在编译质量上:JIT编译器>AOT编译器>解释执行。至于为什么快为什么慢,为什么质量好,为什么质量不好,就留到后面JVM去说吧。
4、Java和C++的区别(5个异同)
- 都是面向对象的语言,都支持继承、封装和多态;
- Java不支持指针来直接访问内存,更加安全;
- Java的类只能单继承,但是接口可以多继承,C++的类能多继承;
- Java有自动管理的垃圾回收机制(GC),不需要手动释放内存;
- C++同时支持方法重载和操作符重载,Java只支持方法重载。
Java基本语法
1、字符型常量和字符串常量的区别?
- 形式上:字符常量是单引号引起来的一个字符,而字符串常量是双引号引起来的0个或者若干个字符
- 内容上:内容上字符串可以直接等价于一个整型的值,参与表达式的运算,而字符串常量代表的是一个地址值
- 占内存大小:字符常量只占两个字节,而字符串常量占若干个字节
注:字符封装类 Character
有一个成员常量 Character.SIZE
值为 16,单位是bits
,该值除以 8(1byte=8bits
)后就可以得到 2 个字节
2、Java中的标识符和关键字有什么区别?
由于大量的需要给程序、类、方法起名字,于是有了标识符。关键字就是特殊的标识符,只能在特定的场合表示特定的功能。以下是Java中常见的关键字:
详细分析一下几个常用的关键字:
1、final关键字
使用final关键字的主要原因在于把方法锁定,以防任何继承类修改它
- 它修饰的类不能被继承,final类中所有的成员方法都会被隐式的指定为final方法;
- final修饰的方法不能被重写,类中所有的private方法都被隐式的执行为了final方法
- final修饰的变量是常量,如果是基本数据类型,一旦定义之后,其值不能被更改,如果是引用数据类型,一旦被定义之后,它不能指向另一个对象。
2、static关键字
主要有以下四个使用场景
- 修饰成员变量和成员方法:这个我们后面已经详细讲了第6点
- 静态代码块:静态代码块定义在类中方法外,执行的顺序为(静态代码块->非静态代码块->构造方法)。该类不管创建多少对象,静态代码只执行一次。这里解释一些代码块,代码块分为三种:静态代码块、构造代码块、普通代码块。静态代码块有static修饰,在类中方法外,随着类的加载而加载,它不能直接访问静态实例变量和实例方法,需要通过类的实例来访问其作用是对静态属性、类进行初始化,并且只执行一次,;构造代码块也在类中方法外,是实例化的时候加载,没有任何修饰字,可以给所有的对象进行初始化;普通代码块是在方法中,执行的顺序与普通的代码语句是一样的,谁在前先执行,其作用是控制变量的生命周期,提高内存的利用率。
- 讲代码块的文章:https://www.cnblogs.com/sophine/p/3531282.html
- 静态内部类:(static关键词只能修饰内部类):静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。
- 讲内部类的文章:https://www.cnblogs.com/dearcabbage/p/10609838.html
- 静态导包(用来导入类中的静态资源,1.5之后的新特性): 格式为:
import static
这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。
3、this关键字
this关键字用于引用类的当前实例,例如:
class Manager {
Employees[] employees;
void manageEmployees() {
int totalEmp = this.employees.length;
System.out.println("Total employees: " + totalEmp);
this.report();
}
void report() { }
}
this的关键字是可选的,上面这个代码在不使用this关键字的情况下表现相同
4、super关键字
super关键字用于从子类访问父类的变量和方法,例如:
public class Super {
protected int number;
protected showNumber() {
System.out.println("number = " + number);
}
}
public class Sub extends Super {
void bar() {
super.number = 10;
super.showNumber();
}
}
super这个关键字主要有以下三个作用:
- 调用父类被子类重写的方法;
- 调用父类被子类重定义的字段(被隐藏的成员变量)
- 调用父类的构造方法
其他情况下,由于子类自动继承了父类响应属性方法,关键字super可以不显示出来
使用this和super要注意的问题:
- this、super不能用在static方法中,因为这两个关键字是属于对象范畴的东西,而静态方法是属于类范畴的东西
- 有关super()调用父类的构造方法时需要注意:编译器会自动在子类构造函数的第一句加上super();来调用父类的无参构造器;此时可以省略不写。如果想写上的话就必须在子类构造器的第一句
- 关于构造方法中的super()有以下几种情形
//情形一 public class A{ public A(String s){ } } public class B extends A{ int a = 0; } //编译失败,此时编译就会报错,因为在父类中有一个有参构造,原先的无参构造不起作用了,所以在本例中JVM给B类加上了一个super(),而父类中却没有无参构造 //情形二 public class A{ public A(String s){ } } public class B extends A{ public B(String s){ } } //编译失败,虽然在子类中定义了一个构造方法,但是在这个构造方法中还是默认调用了super() //情形三 public class A { public A(String s) { } } public class B extends A{ public B(String s) { super(s); } } //编译成功,只要记住,在子类的构造方法中,只要没有显示的通过super去调用父类相应的构造方法,默认调用的都是super(),所以要确保父类中有这个无参构造
讲super关键字的文章:https://blog.csdn.net/shakespeare001/article/details/51388516
有关Java中的修饰符在这里强调一下:
今天同学做DaHua的笔试题,在里面有考察修饰符的使用情况,所以在此记录一下:
Java中的修饰符主要分为以下两类:
- 访问修饰符
- 非访问修饰符
访问修饰符用来保护对类、变量、方法和构造方法的访问。
主要有以下几类:
-
default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
-
private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
-
public : 对所有类可见。使用对象:类、接口、变量、方法
-
protected : 对同一包内的类和不同包的子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
-
需要注意的是:其他采用默认访问修饰符声明的变量和方法,对同一个包内的类是可见的;而接口里的变量都隐式声明为 public static final,而接口里的方法默认情况下访问权限为 public。
-
default和public修饰的是类、变量、接口、方法,而protected和private只能修饰变量和方法。
-
声明为private的方法,不能够被继承
非访问修饰符主要包括:
- static 修饰符,用来修饰类方法和类变量(其实也可以修饰接口和内部类)
- final 修饰符,用来修饰类、方法和变量,通常和 static 修饰符一起使用来创建类常量
- abstract 修饰符,用来创建抽象类和抽象方法。一个类不能同时被 abstract 和 final 修饰
- synchronized 和 volatile 修饰符,主要用于线程的编程。
- 需要进行单独说明的是abstract中的方法分为抽象方法和非抽象方法
- 抽象方法:
抽象方法是一种没有任何实现的方法,该方法的的具体实现由子类提供。
抽象方法不能被声明成 final 和 static。
任何继承抽象类的子类必须实现父类的所有抽象方法,除非该子类也是抽象类。
如果一个类包含若干个抽象方法,那么该类必须声明为抽象类。抽象类可以不包含抽象方法。
-
synchronized 关键字声明的方法同一时间只能被一个线程访问。synchronized 修饰符可以应用于四个访问修饰符。
-
volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值,保证可见性。
3、泛型、类型擦除、通配符
泛型是JDK5引入的一个新机制,用于编译时对类型的安全检查,允许程序员检测到一些非法的类型,实质上是类型参数化,就是将操作的类型指定为一个参数。
Java的泛型是一个伪泛型,就是说Java在编译完成泛型都会被擦掉,称为类型擦除。声明了泛型的 .java 源代码,在编译生成 .class 文件之后,泛型相关的信息就消失了
往定义了泛型的列表中加入其他数据类型的元素会出错,但是可以通过反射来进行添加,因为反射是在运行期间进行的。
List<Integer> list = new ArrayList<>();
list.add(123);
//直接添加会出错
list.add("abc");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//通过反射来进行添加是可以的
add.invoke(list, "abc");
System.out.println(list);
泛型的三种使用方式:泛型接口、泛型类以及泛型方法
1.泛型接口:
public interface Generator<T>{
public T method();
}
1)实现泛型接口不指定类型:
class GeneratorImpl<T> implements Generator{
@Ovrride
public T method(){
return null;
}
}
2)实现泛型接口指定类型:
class GeneratorImpl implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
2.泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
实例化泛型类:
Generic<Integer> genericInteger = new Generic<Integer>(123456);
3.泛型方法
public static <E> void printArray(E[] inputArray) {
for (E element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
使用:
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray(intArray);
printArray(stringArray);
常用的通配符为: T,E,K,V,?
- ? 表示不确定的 java 类型
- T (type) 表示具体的一个 java 类型
- K V (key value) 分别代表 java 键值中的 Key Value
- E (element) 代表 Element
参考:https://www.cnblogs.com/robothy/p/13949788.html
4、==和equals以及hashcode三者之间的联系
equals的正确用法:
equals为了避免空指针异常,最好使用”string“.equals(str)
不过更推荐
Object.equals(null, "string");
可以看一些java.util.Objects中equals的源码:
public static boolean equals(Object a, Object b) {
// 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。
return (a == b) || (a != null && a.equals(b));
}
有关null需要知道的几点:
- 每种原始类型都有默认值一样,如int默认值为 0,boolean 的默认值为 false,null 是任何引用类型的默认值,不严格的说是所有 Object 类型的默认值。
- 可以使用 == 或者 != 操作来比较null值,但是不能使用其他算法或者逻辑操作。在Java中
null == null
将返回true - 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常
5、Java中的数据类型、对应的包装类型,各自占用多少字节
类型:数字类型、字符类型、布尔类型
八大数据类型
1.数字类型:byte、short、int、long、float、double
2.字符类型:char
3.布尔类型:boolean
各种数据类型占的字节数及其默认值:
注意:
Java 里使用 long
类型的数据一定要在数值后面加上 L,否则将作为整型解析
八大包装类:Byte、Short、Integer、Double、Float、Long、Character、Boolean
基本数据类型都直接存在JVM中的Java虚拟机栈中,而包装类属于对象类型,都会存在Java堆中
自动装箱和自动拆箱
Integer i = 10;//装箱
int n = i;//拆箱
装箱就是直接调用了包装类的valueOf方法,而拆箱就是调用了xxxValue的方法
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
;
包装类和常量池
是为了方便快捷地创建(获取)某些对象而出现的。当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),则在需要重复创建相等变量时节省了很多时间。Byte,Short,Integer,Long这四种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在[0,127]范围的缓存数据,Boolean
直接返回 True
Or False
如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡,所以当我们对包装类对象之间的值进行比较的时候,别想着没有超出常量池的范围就能直接用"=="了,最好用equals。
两种浮点数类型的包装类 Float
,Double
并没有实现常量池技术。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Integer i1 = 200;
Integer i2 = 200;
System.out.println(i1 == i2);// 输出 false
Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//false,因为前者是直接从常量池中拿的,相当于Integer.valueOf,而后者相当于重新在堆中创建了对象
BigDecimal
浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。 具体原理和浮点数的编码方式有关,这里就不多提了,我们下面直接上实例:
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false
为了防止精度丢失,就引入了BigDecimal,采用它来定义浮点数的值,再进行浮点数的运算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
用到的函数:
1.比较
a.compareTo(b)
: 返回 -1 表示 a
小于 b
,0 表示 a
等于 b
, 1表示 a
大于 b
。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.compareTo(b));// 1
2.保留几位小数:
BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN);
System.out.println(n);// 1.255
注意:
最好不要使用new BigDecimal(Double)来创建对象,而是需要将字符串作为参数传递进去,或者直接采用BigDecimal.valueOf(double)来进行基本数据类型到BigDecimal的转换
BigDecimal recommand1 = new BigDecimal("0.1")
BigDecimal recommand2 = BigDecimal.valueOf(0.1)
BigDecimal的实现是使用到了BigInteger的,只是前者增加了小数的概念,主要用于操作浮点数,而后者主要用于操作大整数。
String为什么是不可变的?
String
类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]
,所以String
对象是不可变的
String、StringBuilder、StringBuffer三者之间的关系?
线程安全性:
- String中的对象是不可变的,也就可以理解为常量,线程安全。
- StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
- StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
性能:
- String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象。
- StringBuffer是对自己本身进行操作的,不会产生新的对象。
- StringBuilder相比月StringBuffer性能提升大概有10%-15%,但是要冒着线程不安全的风险。
对三者使用的建议:
- 操作少量的数据: 适用String
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
要是想再说的丰富一些的话,就可以说说三者都是通过字符数组来保存值的,只是String中的字符数组用final修饰,所以不能变动和扩展,而两个SB中的没有,所以可以扩展。还有就是两个SB都是继承了AbstractStringBuilder这个类,构造方法也是一样的,拥有一些共同的对字符串操作的函数。只是StringBuffer对调用的方法加了同步锁,所以实现了线程安全,但是效率稍微会低一些。
Strng str = "AAA";和String str = new String("AAA")有什么区别?
我们知道现在在JDK8中String常量池是放在堆中的,采用前者进行赋值,JVM会先从字符串实例池中查询是否存在"test"这个对象,如果存在,直接把实例池中"test"的地址返回给str。如果不存在,则会在实例池中创建"test"对象,并把该对象的地址返回给str。而采用后者方式进行赋值JVM会先从字符串实例池中查询是否存在"test"这个对象,若不存在则会在实例池中创建"test"对象,同时在堆中创建"test"这个对象,然后将堆中的这个对象的地址返回赋给引用str。若实例池存在则直接在堆中创建"test"这个对象,然后将堆中的这个对象的地址返回赋给引用str。
6、静态成员和非静态成员
静态成员的特点:
- 随着类的加载而加载
- 优先于对象存在
- 被所有对象所共享
- 可以直接被类名调用
注意事项:
- 静态方法只能访问静态成员
- 静态方法中不可以写this,super关键字
- 主函数是静态的
静态成员和非静态成员两者之间的区别:
- 生命周期不同
- 调用方式不同
- 别名不同:成员变量被称为实例变量,而静态变量被称为类变量
- 数据存储位置不同:成员变量被存储在堆内存中;而静态变量存储在方法区。
- 有关类的初始化以及静态成员的访问之间的关系看这个链接:https://www.cnblogs.com/javaee6/p/3714716.html?utm_source=tuicool&utm_medium=referral
在一个静态方法内调用一个非静态成员为什么是非法的?
这就要结合JVM的加载机制来谈了,因为静态方法是在类加载的时候就会飞陪内存,可以直接通过类名进行访问。而非静态方法属于实例对象,只有当对象实例化的时候才存在,然后通过类的实例对象去访问。在类的非静态成员不存在的时候静态成员就已经存在了,所以调用内存中还不存在的非静态成员,属于非法操作。
静态方法和实例方法有什么不同?
- 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
7、Java中的值传递
首先要弄清楚两个概念,按值调用和按引用调用,按值调用(call by value)表示方法接收的是调用者提供的值,按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。
但需要弄清楚的是以上概念并不是等价于值传递和引用传递,值传递和引用传递最大的区别在于前者会创建副本,但后者不会创建副本。举一个比较形象的例子:
你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。
你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。
但是,不管上面哪种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了。那你说你会不会受到影响?而我们在pass方法中,改变user对象的name属性的值的时候,不就是在“砸电视”么。你改变的不是那把钥匙,而是钥匙打开的房子。
我们通过值传递,即使传递的是一个引用或者说一个地址,因为在调用的时候实参把地址复制了一份,传递给了形式参数,所以没有办法对地址进行更改,但是可以通过setter和getter两个方法来更改地址中的存储的内容,因为拷贝之后的引用和之前的引用都指向一个地址中的内容。总而言之值传递和引用传递的区别并不是传递的内容。而是实参到底有没有被复制一份给形参。
这个地方Java Guide讲的不够清楚,推荐看这个链接:
https://blog.csdn.net/bjweimengshu/article/details/79799485
可以看几个例子:
1、基本数据类型
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
结果:
a = 20
b = 10
num1 = 10
num2 = 20
2、引用数据类型
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}
public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}
结果:
1
0
3、对象 不修改其中内容
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Student s1 = new Student("小张");
Student s2 = new Student("小李");
Test.swap(s1, s2);
System.out.println("s1:" + s1.getName());
System.out.println("s2:" + s2.getName());
}
public static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println("x:" + x.getName());
System.out.println("y:" + y.getName());
}
}
结果:
x:小李
y:小张
s1:小张
s2:小李
交换之前:
交换之后:
4、对象 修改其中内容
public static void main(String[] args) {
ParamTest pt = new ParamTest();
User hollis = new User();
hollis.setName("Hollis");
hollis.setGender("Male");
pt.pass(hollis);
System.out.println("print in main , user is " + hollis);
}
public void pass(User user) {
user.setName("hollischuang");
System.out.println("print in pass , user is " + user);
}
结果:
print in pass , user is User{name='hollischuang', gender='Male'}
print in main , user is User{name='hollischuang', gender='Male'}
8、重写与重载
重载一般发生在一个类中,本质上就是同样的一个方法根据输入数据的不同,作出不同的处理
除了方法名其他都可以不同,返回值类型、输入值类型、个数、顺序、访问修饰符都可以不同
重写一般发生在父类和子类之间,输入数据一样,但是要做出有别于父类的响应时,就要覆盖父类的方法
需要注意的是:
1、返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
2、如果父类的访问修饰符是private/final/static,则子类不能重写该方法。但是被static修饰的方法能够在子类中对其进行重载和重定义,而且static可以被子类继承
//不能被重写
class Animal {
public static void walk() { System.out.println("Animal行走方法"); }
}
public class Horse extends Animal {
public void walk() {
System.out.println("Horse四条腿行走");
}
public static void main(String [] args) {
new Horse().walk();
}
}
结果:
This instance method cannot override the static method from Animal
//可以被重载
class Animal {
public static void walk() {
System.out.println("Animal行走方法");
}
}
public class Horse extends Animal {
public void walk(String s) {
System.out.println("Horse四条腿行走");
}
public static void main(String [] args) {
new Horse().walk("");
}
}
//可以被重新定义
class Animal {
public static void walk() {
System.out.println("Animal行走方法");
}
}
public class Horse extends Animal {
public static void walk() {
System.out.println("Horse四条腿行走");
}
public static void main(String [] args) {
new Horse().walk();
}
}
参考链接:https://blog.csdn.net/YoaMy/article/details/51275388
//可以被继承
class A{
public static void a(){
System.out.println("a");
}
}
class B extends A{}
public class Test {
public static void main(String[] args) {
B.a();
}
}
//结果:a
//证明静态方法可以被继承
3、构造方法无法被重写
方法的重写要遵循“两同两小一大”
- “两同”即方法名相同、形参列表相同;
- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等
关于 重写的返回值类型 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
public class Hero {
public String name() {
return "超级英雄";
}
}
public class SuperMan extends Hero{
@Override
public String name() {
return "超人";
}
public Hero hero() {
return new Hero();
}
}
public class SuperSuperMan extends SuperMan {
public String name() {
return "超级超级英雄";
}
@Override
public SuperMan hero() {
return new SuperMan();
}
}
9、成员变量和局部变量的区别
- 从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及static
所修饰;但是,成员变量和局部变量都能被final
所修饰。 - 从变量在内存中存储的方式,如果成员变量是由static修饰的,那么这个成员变量是属于类的没如果没有static修饰是属于实例的。对象存在于堆内存,而局部变量存在于栈内存。
- 从变量的生存时间,成员变量(非静态)是对象的一部分,会随着对象的消亡而消亡,而局部变量随着方法的调用而自动消失。
- 从是否有默认赋值来看,成员变量如果没有赋值,都是会以类型的默认值来进行赋值的(一种情况下除外:被final修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
10、有关类的构造方法
构造方法主要用于完成对类对象的初始化工作。
如果一个类没有构造方法也能执行,因为所有类都有默认的无参构造。如果我们自己写了类的构造方法,那么之前默认的无参构造将不会再起作用,所以如果我们重载了有参构造, 那么最好都要把无参构造给写出来。
特点:
- 没有返回值,但不能用void来声明构造函数。
- 名字和类名是一样的
- 生成类的对象时自动执行,无需调用。
- 虽然不能重写但是能重载。
其他相关知识
1、深拷贝和浅拷贝
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递的拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型新建一个对象,并复制原先的内容给这个对象。
2、面向过程与面向对象的区别
- 面向过程的性能比面向对象的性能要高,因为面向对象中的类中非静态成员的调用都需要先进行实例化,开销比较大,比较消耗资源,所以性能是重要考量因素的场景比如嵌入式开发一般采用的是面向过程。
- 面向对象易维护、易复用、易扩展,因为面向对象有封装、继承、多态三种特性,所以可以设计出低耦合的系统。
面向对象的三大特征:
- 封装:把一个对象的状态信息(即属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。为什么要这么做,比如我们设定一个Person对象,里面有一个属性是age,如果直接提供属性给外界访问的话,年龄就没有限制了,外界可以把年龄设置成负数,但是提供的setter之后可以在上面加一个限制,比如:限制在1-100之内,这样才是合理的。就好比一个空调,空调遥控器就只能在16-30摄氏度之间进行调节。
- 继承:不同类型的对象,相互之间经常会有一定数量的共同点。我们把这些共同点属性集中在一起定义一个父类,然后根据不同类型的对象的各自的特点创建子类,而子类只需要添加自己特有的属性就行,其他直接继承父类的。继承可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间,提高我们开发的效率。
- 多态:指的是一个对象具有多种状态,具体可以表现为父类引用指向子类实例。
多态的特点:
- 编译看左边,运行看右边,指的就是能否编译通过就看左边的父类中有没有这个方法,而真正运行是看右边子类。
- 多态不能调用只在子类存在但父类不存在的方法。
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
多态的优点:
- 消除类型之间的耦合关系
- 可替换性
- 可扩充性
- 接口性
- 灵活性
- 简化性
多态存在的三个条件:
- 继承
- 重写
- 父类引用指向子类对象
多态的例子:
public class Test {
public static void main(String[] args) {
show(new Cat()); // 以 Cat 对象调用 show 方法
show(new Dog()); // 以 Dog 对象调用 show 方法
Animal a = new Cat(); // 向上转型
a.eat(); // 调用的是 Cat 的 eat
Cat c = (Cat)a; // 向下转型
c.work(); // 调用的是 Cat 的 work
}
public static void show(Animal a) {
a.eat();
// 类型判断
if (a instanceof Cat) { // 猫做的事情
Cat c = (Cat)a;
c.work();
} else if (a instanceof Dog) { // 狗做的事情
Dog c = (Dog)a;
c.work();
}
}
}
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void work() {
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void work() {
System.out.println("看家");
}
}
/*输出结果为:
吃鱼
抓老鼠
吃骨头
看家
吃鱼
抓老鼠
*/
来源于:https://www.runoob.com/java/java-polymorphism.html
3、对象实体和对象引用
一个对象引用可以指向0或者1个对象,一个对象可以有n个引用指向它。前者可以看成一根绳子可以系上气球也可以什么都不系,而后者可以看成一个气球可以系多个绳子。
引用相等,是指这两个引用指向的是同一个内存地址,而对象相等,比的是内存中存放的内容是否相等。
4、Object类常见方法总结
这里顺便解释一下实例对象和class对象
- 类相当于实例对象的模板,而实例对象相当于用模板对象制作的一个个产品。
- 而class对象可以描述类的基础信息:属性、方法、接口、注解,并能生成类的实例对象(class对象也可叫做元对象,即描述类信息的对象)。
- Class类的存在是实现反射的关键。
一共11种方法:
public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException//native方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,
//表达式 x.clone() != x 为true,说明对象和它的克隆对象不是一个对象,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,
//所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,
//而wait方法释放了锁 。timeout是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。
//所以超时的时间还需要加上nanos毫秒。
public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作
反射
反射可以赋予我们在运行时分析类以及执行类中方法的能力,通过反射你可以获取任意一个类的所有属性和方法,还可以调用这些属性和方法。这里说反射说的不是很清楚,我们回顾一下JVM中的类加载的步骤,类加载就是中有一步就是通过全限定类名来获取class文件的二进制流、并且在堆中创建一个class对象,我们的反射就是通过对这个class对象进行操作的。
1、反射的优缺点
优点:
可以让代码更加灵活、为各种框架提供开箱即用的便利
缺点:
安全问题,在编译时无视泛型参数的安全检查。因为泛型参数的安全检查发生在编译时,一旦编译完成,泛型就会被擦除,在运行期间是得不到泛型的信息的。而反射恰恰是在运行期间执行,那个时候已经没有泛型信息了。
2、注解和反射之间的关系
一个@Component
注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value
注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
3、获取Class对象的四种方式
如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了四种方式获取 Class 对象:
1.如果知道具体的类的情况下:
Class alunbarClass = TargetObject.class;
但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
2.通过Class.forName()传入类的路径获取:
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");
3.通过对象实例instance.getClass()获取:
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
4.通过类加载器xxxClassLoader.loadClass()传入类路径获取:
Class clazz = ClassLoader.loadClass("cn.javaguide.TargetObject");
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行
4、反射的一些基本操作:
https://snailclimb.gitee.io/javaguide/#/docs/java/basis/%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6
5、Java反射机制的应用场景
- 性能第一
反射包括了一些动态类型,所以JVM无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
- 安全限制
使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如Applet,那么这就是个问题了。
- 内部暴露
由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用---破坏了抽象性,降低了可移植性。
6、Java反射机制的应用场景,比如:
- 工厂模式:Factory类中用反射的话,添加了一个新的类之后,就不需要再修改工厂类Factory了。
- 代理模式
- 数据库JDBC中通过Class.forName(Driver)来获得数据库连接驱动。
- 分析类文件:能得到类中的方法等等。
- 访问一些不能访问的变量或属性:破解别人代码。
参考:https://blog.csdn.net/qq_24549805/article/details/104013083
异常
补充:
1、抛出异常并进行捕获的完整语句格式如下,当然也允许try...catch或者try...finally这两种写法
try{
//throw Exception throw语句抛出明确的异常
}
catch{
//find Exception
//hand of it
}
finally{
//must be run
}
2、try中定义的变量catch和finally不能够访问,三个代码块中变量的作用域为代码块内部,分别独立而不能互相访问,如果要在三个块中都可以访问,则需要将变量定义到这些块的外面。
3、如有多个catch能够捕获所抛出异常,只会执行第一个匹配上的catch块代码。
4、无论异常是否发生,finally都会被执行。
5、Java编译器会要求方法必须声明抛出可能发生的未被捕获的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。由程序错误导致的异常属于运行时异常(RuntimeException);而程序本身没有问题,但由于像I/O错误这类异常导致的异常属于其他异常。程序本身的问题,也就是抛出的运行时异常最好是就地解决。
字节流
这里主要说一说字节流和字符流的区别:
字节流处理的最基本的单元是一个字节,通常用来处理二进制数据。字节流InputStream和OutputStream类均为抽象类,代表了基本的输入字节流和输出字节流。
字符流处理的最基本的单元是一个unicode代码单元(大小为2个字节),通常用来处理文本数据
区别:
- 如定义,处理的基本单元不同
- 字节流默认不使用缓冲区,字符流使用缓冲区
- 字节流通常用于处理二进制数据,不支持直接读写字符,字符流常用于处理文本数据
- 在读写文件需要对文本内容进行处理:按行处理、比较特定字符的时候一般选择字符流;仅仅读写文件,不处理内容,还有音频、图片等媒体文件可以采用字节流。
特征:
- 以Stream结尾的都是字节流,以Reader和Writer结尾的一般是字符流
- InputStream是所有字节输入流的父类,OutputStream是所有字符输入流的父类
- Reader是字符输入流的父类,Writer是所有字符输出流的父类
常见字节流:
- 文件流:FileOutputStream和FileInputStream
- 缓冲流:BufferedOutputStream和BufferedInputStream
- 对象流:ObjectOutputStream和ObjectInputStream
常见的字符流
- 字节转字符流:InputStreamReader和OutputStreamReader
- 缓冲流:PrintWriter和BufferedReader
I/O
https://snailclimb.gitee.io/javaguide/#/docs/java/basis/IO%E6%A8%A1%E5%9E%8B
https://mp.weixin.qq.com/s/p5qM2UJ1uIWyongfVpRbCg
IO的通俗理解可以看一下知乎的这个链接:
https://www.zhihu.com/question/32163005
枚举
Enable GingerCannot connect to Ginger Check your internet connection
or reload the browserDisable in this text fieldRephraseRephrase current sentence46Edit in Ginger×