一、定义类
类中5大成员
语法
[修饰符] class 类名 extends 父类
{
//成员变量(field)
//方法(method)
//构造器(constructor)
//内部类(nested class)
//初始化块
}-------类体
- 修饰符:public、final | abstract。有且仅有。
- 类名:只要是标识符即可。从专业角度要求:多个单词连缀而成,每个单词首字母大写。
1、成员变量
语法
[修饰符] 类型 变量名 [= 初始值]
(1)修饰符
private | protected | public、final、static、(transient:序列化相关)。
(2)类型
任意基本类型或引用类型。
(3)变量名
①驼峰(camerlize)写法,首字母小写,后面每个单词的首字母大写。
②用于描述该类或对象的状态,因此通常建议用名词。
例如:
public class Role {
String name;
int id;
//类型是int[],变量名是authorities
int[] authorities;
}
注意
①
public class 面试题
{
//这里定义没问题
int age = 20;//成员变量
//下面的定义错了
double de;//成员变量
de = 3.4;//赋值语句
}
②成员变量,可以不指定初始值;系统会为之分配默认的初始值,初始值规则与数组元素的初始值完全相同
2、方法
(1)语法
[修饰符] 返回值类型 方法名(形参列表)
{
//代码:定义变量(包括数组)、变量赋值、流程控制
//如果声明了返回值类型,必须有return语句
}-------方法体
①修饰符
private | protected | public、final、static。
②返回值类型
- 任意基本类型或引用类型。
- 可使用void声明没有返回值。
③变量名
- 驼峰(camerlize)写法,首字母小写,后面每个单词的首字母大写。
- 用于描述该类或对象的行为,因此通常建议用动词。
④形参列表
- 每个形参都满足“形参类型 形参名”的格式;多个形参之间用逗号隔开。
形参类型1 形参名1,形参类型2 形参名2,...
- 代表调用方法时要传入的参数。
例如:
public int factorial(int n)
{
// n的阶乘:1*2*3*4*...*n
int result = 1;
for (int i = 0; i < n; i++)
{
result *= iii;
}
return result;
}
(2)方法的所属性
①方法类似于函数,但与函数不同的是,方法不能独立存在,必须定义在类里面。
②定义在类中的方法,从逻辑上来看,
- 如果该方法有static修饰,该方法属于类本身,应该用类调用。
- 如果该方法无static修饰,该方法属于对象本身。
③方法不能独立执行,一定要有调用者。
【规则】
- 如果你调用同一个类中方法,可以省略调用者,此时系统会添加默认的调用者。
- 如果该方法是无static方法,添加this作为默认的调用者。
例子:
public class Item {
public void info() {
System.out.println("这是一个商品");
}
//有static方法
private static int add(int a, int b) {
return a + b;
}
public void test() {
//调用同一个类中的非static方法,默认添加this作为调用者
info();//相当于this.info();
//调用同一个类中的static方法,默认添加类作为调用者
add(22, 44);//相当于Item.add(22,44)
}
}
(3)形参个数可变的方法
①语法
类型...形参名
本质就是数组,上面写法等同于类型[] 形参名,然而使用类型[] 形参名不如上面的写法便捷。
②好处
调用方法时更加方便。
- 既可直接传入多个元素,系统会自动将它们封装成数组。
- 也可用数组。
例子:
public class VarArgs {
//个数可变的参数
//如果使用String[]而不使用String...,则无法使用va.test(-2, "hello", "java")
public void test(int a, String... names) {
// TODO Auto-generated method stub
System.out.println("a参数为:" + a);
System.out.println("names数组长度为:" + names.length);
for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}
}
public static void main(String... args) {
// TODO Auto-generated method stub
VarArgs va = new VarArgs();
va.test(-2, "hello", "java");
va.test(34, new String[]{"孙悟空", "猪八戒"});
}
}
③缺点
“类型...”这样的写法只能作为形参列表的最后一个形参。
【暗示】
一个方法最多只能有一个“个数可变的形参”
(4)递归方法
①方法里调用自身——递归带来了隐式循环。
②递归要避免无限递归。一定要在某些情况下。不再调用方法自身
假如我们有:
f(1) = 2;
f(2) = 5;
..
f(n) = f(n + 2) - 2 * f(n + 1);
要计算:f(10)是多少?
令 N = n + 2,
f(N - 2) = f(N) - 2 * f(N - 1);
f(N) = 2 * f(N - 1) + f(N - 2);
public class Recursive {
public static int fn(int n) {
if (n == 1) {
return 2;
}else if (n == 2) {
return 5;
}else {
//递归
//return fn(n + 2) - 2 * fn(n + 1);
//这两个公式完全相同
return 2 * fn(n - 1) + fn(n - 2);
}
}
public static void main(String[] args) {
System.out.println(Recursive.fn(10));
}
}
【难点】
要保证递归一定能出现递归结束的条件。
(5)方法重载(overload)
①同一个类中,有多个同名的方法,但这多个方法的形参列表不同。
②修饰符不同不算重载;返回值类型不同也不算重载。
③当你要确定一个方法时,仅有方法名是不够的,必须要结合参数才能确定。
(6)方法的传参机制
①如果定义方法时声明了形参,调用方法时必须传入对应的参数。
②Java的参数传递机制:值传递,传入的只是参数的副本,并不是参数本身。
③
- 如果传递的参数是基本类型,方法中对参数所做的修改,完全不会影响参数本身。
- 如果传递的参数是引用类型,参数的副本与参数本身指向同一个对象,因此方法通过参数副本修改对象时,会影响参数本身所指向的对象。
例子1-传入参数是基本类型
public class 基本类型传递 {
public static void swap(int a, int b) {
//互换a、b的值
System.out.println("swap开始");
int tmp = a;
a = b;
b = tmp;
System.out.println("swap中a = " + a);
System.out.println("swap中b = " + b);
System.out.println("swap结束");
}
public static void main(String[] args) {
System.out.println("main开始");
int a = 6;
int b = 9;
//传入参数是基本类型
System.out.println("swap之前a = " + a);
System.out.println("swap之前b = " + b);
System.out.println("传入参数是基本类型");
基本类型传递.swap(a, b);
System.out.println("swap之后a = " + a);
System.out.println("swap之后b = " + b);
System.out.println("main结束");
}
}
例子2-传入参数是引用类型
public class 引用类型传递 {
public static void swap(DataWrap dw) {
//互换dw.a、dw.b的值
System.out.println("swap开始");
int tmp = dw.a;
dw.a = dw.b;
dw.b = tmp;
System.out.println("swap中dw.a = " + dw.a);
System.out.println("swap中dw.b = " + dw.b);
System.out.println("swap结束");
}
public static void main(String[] args) {
System.out.println("main开始");
DataWrap dw = new DataWrap();
dw.a = 6;
dw.b = 9;
System.out.println("swap之前dw.a = " + dw.a);
System.out.println("swap之前dw.b = " + dw.b);
//传入参数是引用类型
System.out.println("传入参数是引用类型");
引用类型传递.swap(dw);
System.out.println("swap之后dw.a = " + dw.a);
System.out.println("swap之后dw.b = " + dw.b);
System.out.println("main结束");
}
}
(7)方法重写(override)
子类发现父类不适合自己时,就要重写父类的方法。
【方法重写口诀】:2同2小1大
- 2同:方法名相同、形参列表相同。
- 2小:返回值类型相同或更小,声明抛出的异常相同或更小。
- 1大:访问权限相同或更大。
@Override
作用是报错。要求被修饰的方法必须重写父类方法,否则就报错。
3、构造器
作用:new调用构造器来创建对象
(1)语法
[修饰符] 构造器名(形参列表)
{
//代码:定义变量(包括数组)、变量赋值、流程控制、数据语
}------构造器体
【修饰符】
private | protected | public
(2)构造器规则
①构造器用于初始化对象。
②构造器如何调用?必须用new来调用构造器,这样就可以返回一个初始化完成的对象。
③如果你不为一个类提供构造器,系统会自动为该类提供无参数的构造器。
(3)构造器重载
一个类可以定义多个构造器(因此构造器名必然相同)——必须要求形参列表不同。
【this引用】
this紧跟一个"."。例如:
this.name
this.walk();
【this调用】
- this紧跟圆括号,this(参数)
- 出现在构造器的第一行,调用同一个类中重载的构造器
public Dog(String name, String color) {
this.name = name;
this.color = color;
}
public Dog(String name, String color, int age) {
//this.name = name;
//this.color = color;
this(name, color);
this.age = age;
}
【注意】
- 如果没有为类写构造器,系统会默认为该类提供一个无参构造器。
- 构造器名必须与类名相同。
判断一个类是否为构造器
要看2点:
①构造器名与类名相同
②是否有返回值
例如:
public final class User {
private String name;
int age;
public User(String n)
{
System.out.println("创建User对象");
}
}
【构造器总结】
- 很像一个特殊的方法。没有返回值类型声明,构造器名必须与类名相同。
- 构造器的作用:用于初始化对象——构造器永远属于实例。它不可能用static修饰。
二、类可以用来做什么?
1、定义变量。
所有类都是引用类型。所有类都可以声明变量。
2、调用static修饰方法或static修饰的变量。
3、创建对象
new 构造器(参数)
4、派生子类
三、对象可以用来做什么?
1、调用无static修饰的成员(实例变量)
2、调用无static修饰方法(实例方法)
四、类是引用类型
- 数组也是引用类型。
- Java的引用类型非常多,无穷无尽......只要你定义一个类,就多一个引用类。
- 引用类型的赋值,只是将对象的首地址存入变量中。
五、this引用
this可以出现非static的方法、构造器中。作用如下:
- 出现非static方法中,this代表了该方法的调用者。“谁调用该方法,thsi就代表谁“。
例子:
public class Pig {
String color;
double weight;
public void move() {
System.out.println("猪其实跑得很快");
}
public void test(){
System.out.println("测试方法");
//主调(主语调用者)
//谁在调用test方法,this就代表谁
this.move();
//谁在调用test方法,this就代表谁
System.out.println(this.color);
}
}
public class PigTest {
public static void main(String[] args) {
Pig p1 = new Pig();
p1.color = "白色";
//p1调用test方法,因此test方法中的this代表p1
p1.test();
Pig p2 = new Pig();
p2.color = "黑色";
p2.test();
}
}
- 出现在构造器中,this就代表该构造器正在初始化的对象。
例子:
public class Apple {
String color;
double weight;
//构造器用于对该对象执行初始化
public Apple(String color, double weight) {
//构造器正在初始化谁,this代表谁
this.color = color;
this.weight = weight;
}
}
public class AppleTest {
public static void main(String[] args) {
//此时构造器正在初始化ap,因此Apple构造器中this代表ap
Apple ap = new Apple("黄色", 0.34);
System.out.println(ap.color);
System.out.println(ap.weight);
}
}
this很重要的作用是,用于区分方法或构造器的局部变量(尤其是与成员变量同名时)
六、封装
封装、继承、多态——面向对象的3大特征。
封装包含2方面的意思,
①隐藏:隐藏隐藏内部实现细节。
②暴露
- 将一些操作界面暴露出来。
- 如果通过暴露的界面来操作对象,该对象的内部状态不会被破坏。
简而言之:封装要求合理隐藏、合理暴露
1、访问控制符
- private -> 不写 -> protected -> public
①private(类访问权限):该修饰符修饰的成员,只能在该类中被访问。(彻底隐藏)
②不写(包访问权限):该修饰符修饰的成员,只能在该类及其该类所在包中被访问。(部分隐藏)
③protected(子类访问权限):该修饰符修饰的成员,只能在该类、及其该类所在包、该类的子类中被访问。(部分暴露)
④public(公共):该修饰符修饰的成员可以在任意地方被访问。(彻底暴露)
private | 不写 | protected | public | |
当前类 | √ | √ | √ | √ |
同一个包 | × | √ | √ | √ |
子类 | × | × | √ | √ |
任意 | × | × | × | √ |
【制定原则】
①成员变量(实例变量),通常用private修饰,为了隐藏实现细节。
②为每个成员变量提供public的getter、setter方法,用于控制该成员变量的方法。
③需要暴露的方法,通常用public修饰。
④如果希望一个方法主要用于被子类重写,用protected修饰。
2、包
①不同公司完全可以定义同名的类,为了解决不同公司、不同项目的类名重复的问题。
②Java就引入“包”机制——就是在类名添加一个前缀。
③Java程序为类定义包:
- 在源代码中用package包名
- 将生成class文件要放在对应的文件结构下
(1)包名的命名规范
- 语法要求,只要标识符即可。
- 专业要求:推荐用公司域名倒写,再加项目名。
【备注】
一旦你为类指定了包名之后,使用该类时应该用完整类名:包名+ 类名
(2)导入包
- import的作用:为了省略包名。如果不用import,每次用类时都需要使用包名+类名的形式。
①每次导入一个类
import 包名.类名;
②导入指定包的所有类
import 包名.*;
此时*只能代表类,包名不能用*。
③静态导入
- 每次只导入一个静态成员
import static的作用:可以省略写类名,用于导入指定类的所有静态成员。导入之后,可以省略写类名。
import static 包名.类名.静态成员变量;
- 导入指定类所有静态成员
import static 包名.类名.*;
【备注】
Java程序默认已导入java.lang包下所有类。
七、继承
封装、继承、多态——面向对象三大特征。
1、理解继承
是一种类与类之间的关系。是一种由一般到特殊的父类。
2、继承语法
【继承的好处:代码复用】
[修饰符] class 类名 extends 父类
{
}
【说明】
- Java是单继承,只能有一个直接父类。
- 如果你不显式继承父类,Java默认是继承Object类(JDK系统提供的类)
子类继承父类可以得到父类的:
①成员变量
②方法
八、super限定
与前面this引用非常相似,super用于限定访问父类定义的实例变量或实例方法。
super.父类定义的实例变量
super.父类定义的实例方法(参数)
例子:
九、子类构造器调用父类的构造器
子类构造器一定会调用父类构造器一次——有且仅有一次
①如果子类构造器没有显式调用父类构造器,系统会自动在子类构造器的第一行先调用父类无参数的构造器。
错误例子:父类没有无参数的构造器,所以报错
②子类构造器的第一行显式使用super调用父类构造器。
【super限定】
this紧跟一个"."。例如:
super.name
super.walk();
【super调用】
super紧跟圆括号,super(参数)
【super和this的区别】
- super调用一定是调用父类的构造器。只能出现在构造器的第一行。
- this调用是调用当前类的。只能出现在构造器的第一行。
【super调用和this调用不可能同时出现】
【备注】
- 如果父类没有无参数的构造器,子类的构造器必须显式调用(super调用)父类指定的构造器。
例子:
十、多态
【变态】
同一个类型的实例、在执行同一个方法,个别对象呈现出变异的行为特征。
【多态】
同一个类型的多个实例、在执行同一个方法,呈现出多种的行为特征。
【为什么有多态?】
Java执行方法时,方法的执行是动态绑定的,方法总会执行该变量实际所指向对象的方法。
【变量的类型】
①编译时类型
- 声明该变量是指定的类型。
- 在Java程序的编译阶段,Java编译器只认编译时的类型。
②运行时类型(实际类型)
- 该变量实际所引用的对象的类型。
【向上转型】
子类对象可以直接赋值给父类变量。
自动完成。
【向下转型(强转)】
父类变量赋值给子类变量。
强制转换。
(类型)变量名
【强转运算符的注意点】
①强转运算符只能在具有编译类型具有继承关系的变量之间进行强转,否则编译报错,不兼容的类型。
②如果在编译类型具有继承关系的变量之间转换时,如果被转变量的实际类型,不是要转的目标类型,程序就会引发“ClassCastException”(类型转换异常)
【instanceof运算符】
- 避免ClassCastException异常
- 当变量所引用的对象是后面类或子类的实例时,该运算符返回true。
变量名 instanceof 类型
- instanceof只能在编译类型具有继承关系之间才能进行判断,否则编译报错,不兼容的类型。
- 该运算符可以保证,被强转的变量确实是可转换的才进行转换,从而避免ClassCastException。
例子:
【Java17新增的instanceof模式匹配】
【Java17新增的instanceof模式匹配】
十一、初始化块
满足如下两个特征:
- 初始化块的代码不能传入参数——因为它是隐式调用的。
- 初始化块的代码必须是位于构造器的最前面。
语法
[修饰符static] {
各种语句
}
初始化块是没有名字的
修饰符只能出现一个:static。
- 有static叫类初始化块(静态初始化块)
- 无static叫实例初始化块(非静态初始化块)
1、实例初始化块(没有static)
(1) 实例初始化块是“假象”
一个类在编译之后,实例初始化块就会消失——实例初始化块的代码会被还原到每个构造器的所有代码之前。
例子
public class InitTest {
{
System.out.println("Java");
}
public InitTest() {
System.out.println("无参的构造器");
}
public InitTest(String name) {
System.out.println("带String参数的构造器,参数为:" + name);
}
public static void main(String[] args) {
InitTest in = new InitTest();
InitTest in2 = new InitTest("123");
}
}
相当于
public class InitTest {
public InitTest() {
{
System.out.println("Java");
}
System.out.println("无参的构造器");
}
public InitTest(String name) {
{
System.out.println("Java");
}
System.out.println("带String参数的构造器,参数为:" + name);
}
public static void main(String[] args) {
InitTest in = new InitTest();
InitTest in2 = new InitTest("123");
}
}
(2)实例初始化块的作用
将多个构造器前面部分相同的代码可以提取到实例初始化块中。
(3)实例初始化块何时执行?
只要程序调用构造器创建对象,程序总会先执行实例初始化块——因为实例初始化块被还原到每个构造器的所有代码之前。
(4)定义实例变量时指定的初始值,也是“假象”
- 指定初始值,编译之后就变成构造器所有代码之前的一条赋值语句。
- 实例初始化块的语句要还原到构造器的所有代码之前;定义变量指定的初始值也要还原到构造器的所有代码之前。这二者还原之后的顺序按照它们在源代码中的顺序。
例子
public class InitTest {
//还原到构造器的所有代码之前
{
System.out.println("Java");
age = 20;
}
int age = 2;//还原到构造器的所有代码之前
public InitTest() {
System.out.println("无参的构造器");
}
public InitTest(String name) {
System.out.println("带String参数的构造器,参数为:" + name);
}
public static void main(String[] args) {
InitTest in = new InitTest();
InitTest in2 = new InitTest("123");
System.out.println(in2.age);
}
}
相当于
public class InitTest {
int age;
public InitTest() {
{
System.out.println("Java");
age = 20;
}
age = 2;
System.out.println("无参的构造器");
}
public InitTest(String name) {
{
System.out.println("Java");
age = 20;
}
age = 2;
System.out.println("带String参数的构造器,参数为:" + name);
}
public static void main(String[] args) {
InitTest in = new InitTest();
InitTest in2 = new InitTest("123");
System.out.println(in2.age);
}
}
2、类初始化块(有static)
(1)类初始化块的作用
负责对类执行初始化
- 当程序第一次【主动】使用该类时,系统会为该类分配内存空间、并执行初始化(调用类初始化块)
- 只要你使用该类,基本都算主动使用——除了仅使用类声明变量。
(2)类初始化块何时执行?
- 程序第一次【主动】使用该类时,会执行该类的类初始化块。
- 程序运行时,该类初始化块【只执行一次】
(3)定义类变量时指定的初始值,也是“假象”
- 指定的初始值,编译之后就变成类初始化中的一条赋值语句,但到底是在初始化块的代码之前,还是代码之后,取决于它在源代码中的顺序。
例子
public class StaticInit {
static int age = 30;
//类初始化块
static {
age = 300;
}
}
相当于
public class StaticInit {
static int age;
//类初始化块
static {
age = 30;
age = 300;
}
}
【类初始化块与实例初始化块的区别】
执行次数 | 执行先后 | 何时执行 | |
类初始化块 | 1次 | 先 | 第一次主动用该类 |
实例初始化块 | N次 | 后 | 每次调用构造器 |
例子
class Base{
static {
System.out.println("Base的类初始化块");
}
{
System.out.println("Base的实例初始化块");
}
public Base() {
System.out.println("Base的无参构造器");
}
public Base(String name) {
System.out.println("Base的String参数构造器");
}
}
class Mid extends Base{
static {
System.out.println("Mid的类初始化块");
}
{
System.out.println("Mid的实例初始化块");
}
public Mid() {
super("java");
System.out.println("Mid的无参构造器");
}
public Mid(int age) {
this();
System.out.println("Mid的int参数构造器");
}
}
class Sub extends Mid{
static {
System.out.println("Sub的类初始化块");
}
{
System.out.println("Sub的实例初始化块");
}
public Sub() {
System.out.println("Sub的无参构造器");
}
public Sub(double d) {
this();
System.out.println("Sub的double参数构造器");
}
}
public class Test {
public static void main(String[] args) {
new Sub(3.4);
new Sub(1.0);
}
}
【注意】类初始化块只执行一次
【观点】
- 初始化任何类之前,一定先从Object开始初始化,依次初始化它所有祖先类,最后才到它自己。
- 创建任何对象之前,一定先从Object构造器开始执行,执行它所有祖先类的构造器,最后才执行它自己的构造器。
十二、包装类
Java有8个基本类型:byte、short、int、long、double、char、boolean;这8个基本类型不能当成对象使用,而且也不接受null值。
为了解决上面问题,Java为8个基本类型提供了对应的包装类——可将它们包装成对象。
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
1、自动装箱
- 基本类型的值可以自动当成包装类的实例使用。
【注意】
2、自动拆箱
包装类的实例可以自动当成基本类型的值使用。
【建议】
- 以后做项目时,通常来说建议使用包装类来声明变量。
- 好处是,反正基本类型能做的,它都可以做;它还可以当成对象使用,还可以接受null。
3、包装类的方法
①parseXxx
- 可将字符串转成对应的基本类型值。
- NumberFormatException:要转的字符不符合数值格式,将会引发该异常。
②缓存机制
- 当程序对Integer使用自动装箱时,它有一个缓存机制,它会缓存值-128~127之间的对象。
public class 面试题 {
public static void main(String[] args) {
Integer i = 20;//在-128~127之间,缓存
Integer j = 20;//直接用缓存中对象
System.out.println(i == j);
Integer k = 200;//不在-128~127之间,不缓存
Integer l = 200;//重新创建,不是同一个
System.out.println(k == i);
}
}
十三、Object的两个常用方法
1、toString()方法
- 当你输出一个对象,或将一个对象与字符串进行连接时
——Java默认调用该对象的toString()方法将该对象自动转成字符串。
- 很多时候,都会重写Object的toString()方法。
(1)【默认的toString()】
Object提供的toString返回
类名@hashCode方法返回值
class Apple {
private String color;
private double weight;
//无参数的构造器
public Apple() {
}
//初始化全部成员变量变量的构造器
public Apple(String color, double weight) {
this.color = color;
this.weight = weight;
}
//setter和getter方法
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
// public int hashCode() {
// return 20;
// }
}
public class AppleTest {
public static void main(String[] args) {
Apple ap = new Apple("红色", 2.3);
//下面两行代码完全相同
System.out.println(ap);
System.out.println(ap.toString());
}
}
重写hashcode方法后(20的十六进制是14)
(2)【重写toString()】
@Override
public String toString() {
return "Apple{color=" + color
+ ",weight=" + weight
/*列出所有的成员变量*/
+ "}";
}
2、equals()方法
【equals()与==的区别】
==如果判断两个引用变量,要求两个变量指向同一个对象时,才会返回true。
例子
public class StringEquals {
public static void main(String[] args) {
String str1 = new String("java");
String str2 = new String("java");
//String已经重写了equals方法
System.out.println(str1 == str2);
System.out.println(str1.equals(str2));
}
}
(1)【默认的equals方法】
- Object提供的equals方法判断两个对象相等的标准与==是完全一样的。
(2) 【重写equals()】
- 根据业务规则来提供两个对象相等的标准。
- 实际项目中,用来作为equals比较的关键成员变量,通常并不需要使用全部的成员变量——只要用它们关键的成员变量即可。
class Goat{
private String color;
private double weight;
public Goat() {
}
public Goat(String color, double weight) {
this.color = color;
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
//obj代表被比较对象
@Override
public boolean equals(Object obj) {
//this和obj指向同一个对象
if (this == obj) {
return true;
}
//要求obj不为null
if (obj != null && obj.getClass() == Goat.class) {
//对obj强转为Goat
Goat target = (Goat) obj;
//业务要求有几个关键属性,此处就只比较几个关键属性
return this.color.equals(target.color)
&& this.weight == target.weight;
}
return false;
}
}
public class EqualsTest {
public static void main(String[] args) {
Goat goat1 = new Goat("黑色", 78.2);
Goat goat2 = new Goat("黑色", 78.2);
//goat1与goat2分别指向两个不同的对象,因此==判断返回false
System.out.println(goat1 == goat2);
System.out.println(goat1.equals(goat2));
}
}
十四、static
- static并不是静态的意思。static是类的意思,有static的成员(成员变量、方法、初始化块、内部类)属于类成员,没有static的成员属于实例成员。
1、static是否可以修饰
局部变量 | 外部类 | 成员变量 | 方法 | 构造器 | 初始化块 | 内部类 |
× | × | √ | √ | × | √ | √ |
- 所有类成员都只能用类名调用!
Java语法是不好,允许通过对象来调用类成员,其实没有意义!
面试的笔试题中,如果遇到使用对象来调用类成员的情形,先把对象改成类——题目马上一目了然。
2、static考点
- static成员不能访问非static成员;非static成员可以访问static成员。
- static成员(4种)不能访问非static成员(5种)
例子1——无法从静态上下文中引用非静态成员变量
例子2——无法从静态上下文中引用非静态方法
例子3——无法从静态上下文中引用非静态变量
例子4——无法从静态上下文中引用非静态变量
例子5——无法从静态上下文中引用非静态方法
例子6——无法从静态上下文中引用非静态变量
3、设计模式
对于一批经常出现的设计场景,前人总结出来的比较成功的设计。后面的人就应该学习并模仿,从而提高我们的代码质量。
(1)单例模式
- 在某些场景下,某些类只需要(只能)创建一个实例。
比如系统的窗口管理器、数据库引擎访问点、Java程序所在的JRE环境......都只要产生一个实例
①如何设计单例模式?
- 隐藏构造器——避免被创建实例(不能创建多个实例)。
- 暴露一个static的方法,该方法用于创建实例(该方法还需要保证,该类只会产生一个实例)。
例子
public class Singleton {
private static Singleton s;
//1、隐藏构造器
private Singleton() {
}
//2、暴露一个static方法,用于创建实例
public static Singleton instance() {
if (s == null) {
//还没创建实例
s = new Singleton();
}
return s;
}
}
public class SingletonTest {
public static void main(String[] args) {
//由于隐藏了构造器,这里只能使用static类方法创建实例
Singleton s1 = Singleton.instance();
Singleton s2 = Singleton.instance();
System.out.println(s1.equals(s2));
}
}
十五、final
- 可以修饰变量(各种变量)、方法、类
- final与abstract互斥,永远不能同时出现!
1、final修饰变量
- 该变量被赋初始值之后,不能被重新赋值!
- final修饰的变量必须被赋值,且只能赋值一次
2、final修饰成员变量
(1)非final的成员变量
- 程序员可以不显式指定初始值,系统会为之分配默认初始值,初始值分配规则与数组元素的初始值分配规则完全相同。
【初始值分配规则(数组元素的初始值分配规则)】
整数类型(byte、short、int和long) | 0 |
浮点类型(float、double) | 0.0 |
字符类型(char) | '\u0000' |
布尔类型(boolean) | false |
引用类型(类、接口和数组) | null |
(2)final的成员变量
- 程序员必须显式指定初始值
①final实例变量
必须显式指定初始值。只能在以下3个位置的其中之一指定:
- 定义时指定初始值
- 实例初始化块
- 构造器
上面3个位置的本质只有一个——构造器
例子
②final类变量
必须显式指定初始值。只能在以下2个位置的其中之一指定:
- 定义时指定初始值
- 类初始化块
上面2个位置的本质只有一个——类初始化块
【注意】
- 实例初始化块可以访问final类变量,但不能指定初始值
- final实例变量不能在static方法中指定初始值
3、final修饰局部变量
(1)非final的局部变量
- 程序员必须先显式指定初始值,然后才能使用。
(2)final的局部变量
- 程序员必须先显式指定初始值,然后才能使用。
- final的局部变量不能被重新赋值
4、final修饰的是引用类型的变量
- final只保证该引用变量本身不会被重新赋值,该变量所引用的对象完全可以被修改
例子
5、final修饰的宏替换变量
(1)final修饰的宏替换满足的3个条件
如果一个变量满足以下3个条件,这个变量就会消失(所有出现该变量的地方,在编译时就会替换成该变量的值)
①变量有final修饰
②声明变量时指定了初始值
③变量的初始值可以在编译时确定(初始值的表达式中没有变量)
例子1
public class Final宏变量 {
public static void main(String[] args) {
//有final修饰,指定了初始值,且初始值在编译时即可确定
final int MAX = 100;
System.out.println(MAX);
}
}
- final
-无final
例子2
public class FinalTest {
public static void main(String[] args) {
String st1 = "java";//第一次用字符串,该字符串进入“池”
String st2 = "java";//第二次直接用”池“中字符串
System.out.println(st1 == st2);
String st3 = "java";//第一次用字符串,该字符串进入“池”
String st4 = "ja" + "va";//编译阶段就会计算结果:java。因此会直接使用“池”中字符串
System.out.println(st3 == st4);
String st5 = "ja";
String st6 = "va";
String str = st5 + st6;//st5、st6是变量,要等运行时才能计算结果,因此无法使用“池”中字符串
System.out.println(st3 == str);
final String st7 = "ja";
final String st8 = "va";
String str2 = st7 + st8;//st7、st8会消失,这行代码相当于:String str2 = "ja" + "va";
System.out.println(st3 == str2);
}
}
例子3
public class FinalTest2 {
public static void main(String[] args) {
//下面代码有几个变量?几个对象?
//0个变量,1个对象
final String s1 = "java";
final String s2 = s1 + " is";
final String s3 = s2 + " a";
final String s4 = s3 + " very";
final String s5 = s4 + " good";
final String s6 = s5 + " language";
System.out.println(s6);
//上面代码相当于
System.out.println("java is a very good language");
}
}
6、final修饰方法
- 表明该方法不允许被子类重写——该方法可以被重载,也可以被子类调用
例子1——该方法不允许被子类重写
例子2——该方法可以被重载
例子3——该方法可以被子类调用
【备注】final修饰prviate方法纯属多余,但java是允许的
- private方法已经被隐藏在该类的内部,子类无法访问该方法,因此不可能被重写。
7、final修饰类
- 表明该类不能派生子类。jdk里面很多类都是final:String、Math、System
【Object是否为final?】
- 一定不是final
十六、abstract(抽象)
- 它只能修饰2个东西:方法和类(抽象方法和抽象类)
- abstract与final是互斥的,永远不可能同时出现!
1、抽象类
(1)抽象类与普通类的区别
只有4个字——有得有失:
- 有得:得到一个新功能:抽象类可拥有抽象方法,普通类不可以拥有抽象方法
- 有失:抽象类失去了一个功能:创建对象
(2)抽象类的作用
①定义变量,只能用它的子类的实例,向上转型
②调用类方法和类变量
③派生子类——主要目的
子类构造器一定要调用父类构造器一次,因此抽象类必须有构造器
例子
public abstract class Animal {
//抽象方法
public abstract void move();
public static void info() {
System.out.println("我是一个动物");
}
}
public class Bird extends Animal{
@Override
public void move() {
System.out.println("鸟在天上飞");
}
}
public class Camerl extends Animal {
@Override
public void move() {
System.out.println("骆驼在沙漠走");
}
}
public class AnimalTest {
public static void main(String[] args) {
Animal.info();//调用类方法
//向上转型
Animal an1 = new Bird();
Animal an2 = new Camerl();
//an1、an2的类型是Animal,体现出多态
an1.move();
an2.move();
}
}
(3)抽象类派生子类
【规则】
- 子类要么重写抽象父类中所有的抽象方法,要么子类也只能是抽象的。
2、抽象方法
- 只有方法签名,没有方法体的方法
- 一定要交给子类去实现,否则不能调用