Java对象和实例的关系:
面向对象编程(Object-Oriented Programming),是对现实世界建立计算机模型的一种方法。
class是对象的模板,它定义了如何创建实例,class的名字就是数据类型。一个class里可以有多个字段(field),字段用来描述class的特征。
instance是对象的实例,它根据class创建,可以有多个实例,它们的类型相同,但各自的属性可能不同。每个实例都有自己独立的存储空间。
使用new操作符就可以创建一个实例。通过变量.字符可以访问或修改实例的字段数据。
如:
Main.java中:
package com.company;
public class Main {
public static void main(String[] args) {
Person xiaoming=new Person();
xiaoming.name="小明";
xiaoming.age=12;
Person xiaohong=new Person();
xiaohong.name="小红";
xiaohong.age=15;
System.out.println(xiaoming.name);
System.out.println(xiaoming.age);
System.out.println(xiaohong.name);
System.out.println(xiaohong.age);
}
}
Book.java中:
package com.company;
public class Book {
public String name;
public String author;
public String isbn;
}
Person.java中:
package com.company;
public class Person {
public String name;
public int age;
}
请把三个.java文件放在同一目录下。
运行截图如下:
Java数据封装:
方法:
方法封装了访问实例字段的逻辑。
方法是一组执行语句,遇到return返回。void表示不返回任何值(注意区分null)。方法可以让外部代码安全地访问实例字段。
一个class里可以有多个字段(field),如果直接把field用public修改,会直接把field暴露给外部,可能会破坏封装。当用private修饰field时,可以拒绝外部访问。此时我们可以在class内定义修改private修饰的field的方法,用public来修饰该方法,这就可以间接设置和获取private修饰的field。通过方法来访问更加安全。
我们通过变量.方法名()来调用实例方法。
方法名称:
首单词字母小写,后面单词首字母大写。
方法内部可以使用隐式变量this:
this指向当前实例;
this.field可以访问当前实例的字段。
在不引起歧义的情况下,我们可以省略this。
在有局部变量名时,局部变量名优先。
方法参数是用于接收传递给方法的变量值。可以是基本类型参数,也可以是引用类型参数。
private方法:
外部代码不可访问private方法。
内部代码可以调用自己的private方法。
方法重载:
方法重载即多个方法的方法名相同,但各自的参数个数、类型、位置不同。方法的返回值类型通常是相同的。
方法重载的目的是相同功能的方法使用同一名字,便于调用。重载方法主要依靠参数类型和数量区分。注意不要交换参数顺序。
Java继承和多态:
继承:
Java中所有类都继承自Object类。
我们创建一个Person类,然后再创建一个Student类时,就可以让Student类继承Person类。
如:
Java只允许class继承一个类,即一个类有且仅有一个父类(Object类除外)。
如:
需要注意的是,Person类定义的private字段无法被子类访问。而Person类中用protected修饰的字段可以被子类访问。
super关键字:
在Java中,子类的构造方法第一句必须调用父类的构造方法,即子类的构造方法第一行语句必须是super()。super关键字表示父类(超类)。
如果没有写super(),编译器会自动生成super()。如果父类没有默认构造方法,子类则必须显示调用super(),并传入super需要的参数。
向上转型:
实例变量可以进行向上转型。
如:
最下面三行代码实现了把一个子类型实例转变为父类型实例。
向下转型:
如:
向下转型把一个父类型实例转变成子类型实例。
instanceof操作符可以判断对象类型:注意判断是是判断某个对象是否是某个类型或这个类型的父类。
如:
如果一个变量值为null,则instanceof判断结果始终为false。我们可以在向下转型前先用instanceof判断。
区分继承和组合:
如:
可以看出Student类不适合从Book类继承,我们可以让Student类从Person类继承,但在Student类中声明一个Book类实例(这就是组合)。
继承是is关系,组合是has关系。
多态:
子类中重新定义一个与父类中同名的方法,叫做覆写(Override)。
子类调用同名的方法时,如果有覆写。调用的是本子类覆写后的方法。
如:
我们可以加上@Override来让编译器帮助检查是否进行了正确的覆写。
多态指针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。对某个类型调用某个方法,执行的方法可能是某个子类的覆写方法。
利用多态,允许添加更多类型的子类实现功能扩展。
Object类:
几个重要方法:
toString:把instance输入为String。
equals:判断两个instance是否逻辑相等。
hashCode:计算一个instance的哈希值。
如何调用父类中被覆写的方法?
如:
使用super关键字可以调用父类被覆写的方法。
final关键字:
用final修饰的方法不能被覆写,用final修饰的类不能被继承,用final修饰的字段在初始化后不能被修改(必须创建对象时就初始化)。
Java抽象类和接口:
抽象类:
我们已经知道每个子类都可以覆写父类的方法。
如果父类的方法没有实际意义,能否去掉方法的执行语句?
如:
这时会报编译的错误。
如果把Person类的run方法全部注释掉,就会丧失多态的特性。
我们可以把父类的方法声明为抽象方法,使用abstract关键字。
如:
这时编译仍然会报错,我们还需要把Person类也声明为抽象类。
如果一个class定义了方法,但没有具体的执行代码,这个方法就是抽象方法。抽象方法用abstract修饰,抽象方法也没有任何执行语句。
因为无法执行抽象方法,所以这个类也必须申明为抽象类。我们无法实例化一个抽象类。但可以实例化一个抽象类的子类。
如:
无法实例化的抽象类的作用:
这个抽象类用于被继承,抽象类可以强迫子类实现其定义的抽象方法。抽象方法实际上相当于定义了“规范”。
面向抽象编程的本质:
上层代码只定义规范(abstract class Person),不需要子类就可以实现业务逻辑(正常编译),具体的业务逻辑由不同的子类来实现,调用者并不关心。
如:
这个代码可以正常编译。
总结:
抽象方法定义了子类必须实现的接口规范;
定义了抽象方法的类就是抽象类;
从抽象类继承的子类必须实现抽象方法;
如果不实现抽象方法,则该子类仍然是一个抽象类。
接口:
如果一个抽象类没有字段,且所有方法都是抽象方法,我们就可以把该抽象类改写为接口(interface)。
在Java中,我们使用interface来声明一个接口。
接口定义的方法默认是public abstract(不需要写)。
interface是Java内置的纯抽象接口,实现interface时要使用implements关键字,我们可以实现多个interface。
注意:
在Java中,接口特指interface定义的接口,只定义方法签名。
如:
抽象类和接口的区别:
在interface中定义的default方法,在子类中就可以不用实现这个default方法而不会产生编译错误。
一个interface也可以继承自另一个interface,interface继承自interface使用extends关键字。这相当于扩展了接口的方法。
如:
我们需要合理设计interface和abstract class的继承关系。一般我们把公共逻辑放在abstract class中。接口层次代表抽象的程度。
如:
总结:
接口定义了纯抽象规范;
类可以实现多个接口;
接口也是数据类型,适用于向上转型和向下转型;
接口不能定义实例字段,但可以定义default方法(JDK>=1.8)。
Java静态字段和方法:
静态字段:
用static修饰的字段称为静态字段。
普通字段在每个实例中都有自己的一个独立空间,而静态字段只有一个共享“空间”,所有实例都共享该字段。
如:
number只有一个“共享”空间。
所有实例共享一个静态字段。我们不推荐用实例变量访问静态字段,推荐用类名来访问静态字段。我们可以把静态字段理解为描述class本身的字段(非实例的字段)。
如:
静态方法:
用static修饰的方法称为静态方法。
调用实例方法必须通过实例变量,而调用静态方法不需要实例变量。静态方法就类似C语言中的函数。
如:
注意:
静态方法不能访问this变量;
静态方法不能访问实例字段(实例字段实际都是通过this变量来访问的);
静态方法可以访问静态字段和调用其他静态方法。
静态方法经常用于工具类Arrays.sort()和Math.random(),静态方法经常用于辅助方法,我们Java程序的入口main()也是静态方法。
总结:
静态字段属于所有实例“共享”的字段,实际上是属于class的字段。
调用静态方法不需要实例,因此无法访问this。
静态方法可以访问静态字段和其他静态方法。
静态方法常用于工具类和辅助方法。
Java包和作用域:
包:
如何解决同样的类名的冲突?
Java中使用package来解决这个冲突,Java定义了名字空间:包;包名+类名=完整类名。
如:
JVM加载class并执行代码时,总是使用class的完整类名,因此只要包名不同,类就不同。包可以是多层结构,但包没有父子关系。
编译器编译后的class文件中全部是完整类名。
如:
包作用域:
位于同一个包的类,可以访问包作用域的字段和方法。不用public、protected、private修饰的字段和方法就是包作用域。
如:
引用其他类的方法:
1、使用完整的类名。
如:
2、使用import语句先import包,再使用类名。
如:
作用域:
Java的类、接口、字段和方法都可以设置访问权限。访问权限有public、protected、private和package四种。
定义为public的field和method可以被其他类访问。如果不确定是否要public,就不声明public,减少对外暴露方法。
定义为private的field和method无法被其他类访问。private访问权限限定在class内部,与方法声明顺序无关。一般推荐把private方法写在public方法的后面。
private也可以修饰class,定义为private的class无法被其他类访问,访问private class被限定在外层class的内部。定义在一个class内部的class成为内部类(inner class)。
如:
protected作用于继承关系,定义为protected的字段和方法可以被子类访问。
如:
package作用域指一个类允许访问同一个package的:
没有public、private修饰的class;
没有public、protected、private修饰的字段和方法。
因为包没有父子关系,所以包名必须完全一致。
如:
局部变量:
在方法内部定义的变量称为局部变量。局部变量作用域从变量声明处开始到对应块结束。
在编写代码时,我们应当尽可能把局部变量的作用域缩小,并尽可能延后声明局部变量。
如:
final关键字:
final与访问权限不冲突;
用final修饰class可以阻止被继承;
用final修饰method可以阻止被覆写;
用final修饰field可以阻止被重新赋值;
用final修饰局部变量可以阻止被重新赋值。
总结:
Java内建的访问权限包括public、protected、private和package。
Java在方法内部定义的变量是局部变量。
局部变量的作用域从变量声明开始,到一个块结束。
final修饰符不是访问权限。
一个.java文件只能包含一个public class,但可以包含多个非public class。
Java的classpath和jar:
classpath:
classpath是一个环境变量,指示JVM如何搜索class。设置的搜索路径与操作系统有关。
如:
我们可以在系统环境变量中设置classpath环境变量(不推荐),或者在启动JVM时设置classpath变量(推荐):
如:
如果没有设置环境变量,也没有设置-cp参数,默认的classpath是当前目录。
jar包:
jar包是zip格式的压缩文件,包含若干.class文件。jar包相当于目录。jar包还可以包含其他jar包。
classpath中可以包含jar文件,如:
使用jar包可以避免大量的目录和class文件。
如何创建jar包?
1、使用JDK自带的jar命令;
2、使用构建工具Maven等。
3、由于jar包是zip格式的压缩文件,我们可以使用压缩文件来直接创建.zip文件,然后修改后缀名为.jar。
JDK的class:
当JVM运行时会自动加载JDK自带的class,JDK自带的class被打包在rt.jar。rt.jar由JVM直接加载,我们不需要在classpath中引用rt.jar。
Java核心类:
String:
String可以直接使用"..."表示,String一旦创建内容不可变。
equals(Object)方法可以比较两个String的内容是否相等。equalsIgnoreCase(String)方法忽略大小写来比较两个字符串的区别。
如:
String常用操作:
是否包含子串:contains/indexOf/lastIndexOf/startsWith/endsWith;
去除首尾空白字符:trim。注意trim()不改变字符串内容,而是返回新字符串;
提取子串:substring();
大小写转换:toUpperCase()/toLowerCase();
替换子串:replace()/replaceAll();
分割:split();
拼接:join();
任意数据转换为String:static String valueof(int/boolean/Object);
String转换为其它类型:static int Integer.parselnt(String)或static Integer Integer.ValueOf(String)。
全球统一编码:Unicode,全球所有文字都有唯一编码。Java程序运行时使用Unicode编码。
由于英文的Unicode编码和ASCII码不一致(前者占两个字节,后者占一个字节),当有大量英文文本时会浪费空间,故又出现了UTF-8编码。
UTF-8编码是一种变长编码,当是英文字符时,UTF-8编码也是一个字节;当是中文时,往往是3个字节。该编码的容错能力很强。
总结:
Java中字符串是不可变对象;
字符串操作不改变原字符串内容,而是返回新字符串;
字符串和byte[]互相转换时要注意编码,建议总是使用UTF-8编码。
StringBuilder:
String可用+拼接,但每次拼接都会创建一个新的字符串对象,这浪费了内存。
如:
StringBuilder是一个可变对象,它可以预先分配缓冲区,故可以高效拼接字符串。StringBuilder支持链式操作。
如:
当只有一行代码时,不需要特别改写字符串+操作,因为编译器会在内部自动把多个连续的+操作优化为StringBuilder操作。
如:
StringBuffer接口和StringBuilder接口完全相同,StringBuffer是StringBuilder的多线程安全版本。因为跨线程做字符串拼接很少见,所以我们不需要用StringBuffer。
总结:
StringBuilder是可变对象,用来高效拼接字符串;
StringBuilder可以支持链式操作,实现链式操作的关键是返回实例本身;
StringBuffer是StringBuilder的线程安全版本,很少使用。
包装类型:
Java数据类型分成基本类型(int等)和引用类型(所有class类型),基本类型不能看成对象。
但是我们可以定义一个Integer类,包含一个实例字段int,这样就可以把Integer视为int的包装类型(wrapper)。
如:
事实上,JDK为每种基本类型都创建了对应的包装类型:
int、Integer和String的相互转换:
注意:
Integer.getInteger(String)是从系统环境中读取系统变量。
编译器可以自动在int和Integer之间转型:
自动装箱:auto boxing,int->Integer
自动拆箱:auto unboxing,Integer->int。
自动装箱和自动拆箱只发生在编译阶段,装箱和拆箱会影响执行效率。编译后的class代码时严格区分基本类型和引用类型的。当Integer->int执行时可能会报错(比如Integer值为null时)。
如:
包装类型还定义了一些有用的静态变量。
整数和浮点数的包装类型继承自Number类。
总结:
JDK包装类型可以把基本类型包装为class;
自动装箱和自动拆箱是编译器完成的(KDK>=1.5),装箱和拆箱会影响执行效率;
拆箱时可能发生NullPointerException。
JavaBean:
许多class往往是这么定义的:定义若干的private实例字段,然后通过public方法读写实例字段。
如:
符合这种命名规范的class被称为JavaBean:
private Type field;
public Type getField();
public void setField(Type value);
通常把一组对应的getter和setter方法称为属性。只有getter的属性称为只读属性。只有setter的属性称为只写属性。属性只需要定义getter和setter方法,不一定要定义对应的field。
如:
上面例子中,name属性对应读方法getName(),对应写方法setName()。
JavaBean方便IDE工具读写属性,方便传递数据。
总结:
JavaBean是一种符合命名规范的class;
JavaBean通过getter/setter来定义属性,属性是一种通用的叫法,并非Java语法规定;
可以利用IDE快速生成getter/setter;
可以使用Introspector.getBeanInfo()获取属性列表。
枚举类:
如何定义常量:
一般用static final来定义。
如:
这样定义有个缺点,就是编译器无法检查定义的常量的内容。
Java提供了一个enum关键字来定义常量类型,常量本身带有类型信息,常量可以用==来比较。
如:
enum定义的类型实际上是class,继承自java.lang.Enum,我们不能通过new来创建实例,所有常量都是唯一实例(引用类型)。可以用于switch语句。
如:
需要注意的是,如果直接写下面的class是不能编译通过的。
总结:
enum可以定义常量类型;
name()获取常量定义的字符串,注意不要用toString();
ordinal()返回常量定义的顺序;
我们可以为enum类编写构造方法、字段和方法,构造方法申明为private。
常用工具类:
Math:
Math提供了数学计算的静态方法:
abs/min/max;
pow/sqrt/exp/log/log10;
sin/cos/tan/asin/acos...;
常量:
PI=3.14159...
E=w.27828...
Math.random()生成一个随机数,这个数在[0,1)范围内。我们可以用这个来生成某个区间的随机数。
如:
Random类:
Random用来创建伪随机数,有以下方法:
nextInt/nextLong/nextFloat...;
nextInt(N)生成不大于N的随机数。
如:
伪随机数的意思是给定同一个种子伪随机数算法会生成完全相同的序列,不给定种子时Random使用系统当前时间戳为种子。
Math.random()方法在内部也调用了Random类,故生成的也是伪随机数。只是我们不能指定其种子。
SecureRandom可以用来创建安全的随机数,只是速度较慢。
如:
BigInteger用任意多个int[]来表示非常大的整数。
如:
类似的,BigDecimal表示任意精度的浮点数。
如:
总结:
Math:数学计算;
Random:生成伪随机数;
SecureRandom:生成安全的随机数;
BigInteger:表示任意大小的整数;
BigDecimal:表示任意精度的浮点数;
BigInteger和BigDecimal都继承自Number。