前言:此为个人笔记梳理,如若侵权,请联系作者删除
1.面向对象的概念
面向对象的三大特征
- 封装:对外部不可见,可以保护程序中的某些内容
- 继承:扩展类的功能
- 多态:方法的重载,对象的多态性
类与对象的关系(对象是类的具体)
面向对象的核心组成部分:类与对象
1.类是对某一类事物的描述,是抽象的、概念上的定义
2.对象是实际存在的该类事物的每一个个体,因而也称为实例(instance)
- 定义类和对象
类的定义格式:
class 类名称 {
数据类型 属性; //声明成员变量(属性)
... ...
public 返回值的数据类型 方法名称(类型 参数1,类型 参数2...) { //定义方法的内容
程序语句;
[return 表达式;]
}
}
class Person {
String name; //表示姓名
int age; //表示年龄
public void tell() {
System.out.println("姓名:" + name + ";年龄:" + age);
}
}
注:此时定义了一个类,一个类定义完成之后不能直接使用,需要产生对象。对象也需要使用其固定的产生格式,才可以使用
- 对象的创建及使用
类名 对象名称 = null; //声明对象
对象名称 = new 类名(); //实例化对象
类名 对象名称 = new 类名();
public class ClassDemo02 {
public static void main(String[] args) {
Person per = new Person();
}
}
Person 类图
Person |
name : String |
age : int |
+tell(): void |
注:“+”为访问权限:public
所示的图形分为三个层次:
第一层:表示类的名称,类的名称与之前一样要求开头首字母大写;
第二层:表示属性的定义,按照“访问权限 属性名称 : 属性数据类型”的格式定义;
第三层:表示类中方法的定义,按照“访问权限 方法名称() : 方法返回值”的格式定义。
- 类与对象的进一步研究(类属于引用传递类型)
内存划分:对象创建之初
Person per = new Person();
声明对象:Person per,栈内存中声明的。与数组一样,数组名称就保存在栈内存之中,只开辟了栈内存的对象是无法使用的,必须有其堆内存的引用才可以使用
实例对象:new Person(),在堆中开辟空间,所有的内容都是默认值
String:是一个字符串,本身是一个类,就是一个引用数据类型,则此时默认值就是null
int:是一个整数,本身就是一个数,所以是基本数据类型,则此时的默认值就是0
注:开发中,数据最好有一个初值,这样操作会比较方便
了解了其分配的关系,就可以为属性赋值,并调用类中的方法
使用格式:
调用属性:对象.属性;
调用方法:对象.方法();
class Person {
String name; //表示姓名
int age; //表示年龄
public void tell() {
System.out.println("姓名:" + name + ";年龄:" + age);
}
}
public class ClassDemo03 {
public static void main(String[] args) {
Person per = null;
per = new Person();
per.name = "张三";
per.age = 30;
per.tell();
}
}
打印结果:
姓名:张三;年龄:30
在程序中所有的方法是保存在全局代码区之中的,此区中的内容是所有对象共享的
注:在使用对象的时候,对象必须被实例化之后才可以使用(实例化对象,并不单单指的是直接通过 new 关键字实现的,只要其有堆内存的空间指向,则表示实例化成功)
- 空指向异常
在引用操作中,如果一个对象没有堆内存的引用,而调用了类中的属性或方法,就会出现此问题
class Person {
String name; //表示姓名
int age; //表示年龄
public void tell() {
System.out.println("姓名:" + name + ";年龄:" + age);
}
}
public class ClassDemo04 {
public static void main(String[] args) {
Person per1 = null;
Person per2 = null;
per1 = new Person();
per2 = new Person();
per1.name = "张三";
per1.age = 30;
per2.name = "李四";
per2.age = 33;
System.out.println("per1对象中的内容-->");
per1.tell();
System.out.println("per2对象中的内容-->");
per2.tell();
}
}
打印结果:
per1对象中的内容-->
姓名:张三;年龄:30
per2对象中的内容-->
姓名:李四;年龄:33
class Person {
String name; //表示姓名
int age; //表示年龄
public void tell() {
System.out.println("姓名:" + name + ";年龄:" + age);
}
}
public class ClassDemo05 {
public static void main(String[] args) {
Person per1 = null;
Person per2 = null;
per1 = new Person();
per2 = per1; //把per1堆内存的使用权给per2
per1.name = "张三";
per1.age = 30;
System.out.println("per2对象中的内容-->");
per2.tell();
}
}
打印结果:
per2对象中的内容-->
姓名:张三;年龄:30
- 另一道引用传递(GC:垃圾收集机制的简称)
class Person {
String name; //表示姓名
int age; //表示年龄
public void tell() {
System.out.println("姓名:" + name + ";年龄:" + age);
}
}
public class ClassDemo06 {
public static void main(String[] args) {
Person per1 = null;
Person per2 = null;
per1 = new Person();
per2 = new Person();
per1.name = "张三";
per1.age = 30;
per2.name = "李四";
per2.age = 33;
per2 = per1;
System.out.println("per1对象中的内容-->");
per1.tell();
System.out.println("per2对象中的内容-->");
per2.tell();
}
}
打印结果:
per1对象中的内容-->
姓名:张三;年龄:30
per2对象中的内容-->
姓名:张三;年龄:30
因为 per2 改变了指向,所以其原本的内存空间就没有任何栈的引用,则这样的内存就被称为垃圾,等待着垃圾收集机制进行回收
2.封装性
封装的目地:封装就是保护内容,保证某些属性或方法可以不被外部看见
class Person {
String name; //表示姓名
int age; //表示年龄
public void tell() {
System.out.println("姓名:" + name + ";年龄:" + age);
}
}
public class EncDemo01 {
public static void main(String[] args) {
Person per = null;
per = new Person();
per.name = "张三";
per.age = -30;
per.tell();
}
}
打印结果:
姓名:张三;年龄:-30
根本原因:此处让属性可以直接被外部所访问
- 实现封装
为属性封装:private 属性类型 属性名称;
为方法封装:private 方法返回值 方法名称(参数列表) {...}
class Person {
private String name; //声明姓名属性
private int age; //声明年龄属性
public void tell() {
System.out.println("姓名:" + name + ";年龄:" + age);
}
}
public class EncDemo02 {
public static void main(String[] args) {
Person per = null;
per = new Person();
per.name = "张三";
per.age = -30;
per.tell();
}
}
打印结果:
EncDemo02.java:12: 错误: name可以在Person中访问private
per.name = "张三";
^
EncDemo02.java:13: 错误: age可以在Person中访问private
per.age = -30;
^
2 个错误
编译将出错:提示是一个私有的访问权限,则意味着,外部根本就无法调用
- 访问封装的内容
封装属性的原则:需被访问,使用 setter 及 getter 完成
例如:有一个属性:private String name;
Setter(设置):public void setName(String n) {...}
Getter(取得):public String getName() {...}
class Person {
private String name; //声明姓名属性
private int age; //声明年龄属性
public void setName(String n) {
name = n;
}
public void setAge(int a) {
age = a;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void tell() {
System.out.println("姓名:" + name + ";年龄:" + age);
}
}
public class EncDemo03 {
public static void main(String[] args) {
Person per = null;
per = new Person();
per.setName("张三");
per.setAge(-30);
per.tell();
}
}
打印结果:
姓名:张三;年龄:-30
注:以上的代码只是可以访问封装属性了,但是并没有加入到检测的措施。如果年龄设置有问题,则不应该为属性赋值
在 setter 方法中加入验证,在 getter 方法中只是把值返回
class Person {
private String name; //声明姓名属性
private int age; //声明年龄属性
public void setName(String n) {
name = n;
}
public void setAge(int a) {
if(a>=0&&a<=150) { //加入验证
age = a;
}else {
System.out.println("年龄不符合要求啊!");
}
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void tell() {
System.out.println("姓名:" + name + ";年龄:" + age);
}
}
public class EncDemo04 {
public static void main(String[] args) {
Person per = null;
per = new Person();
per.setName("张三");
per.setAge(-30);
per.tell();
}
}
打印结果:
年龄不符合要求啊!
姓名:张三;年龄:0
封装的类图表示
Person |
-name : String |
-age : int |
+tell(): void |
+getName(): String |
+setName(String n): void |
+getAge(): int |
+setAge(int a): void |
class Person {
private String name; //声明姓名属性
private int age; //声明年龄属性
public void setName(String n) {
name = n;
}
public void setAge(int a) {
if(a>=0&&a<=150) { //加入验证
age = a;
}else {
System.out.println("年龄不符合要求啊!");
}
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void tell() {
System.out.println("姓名:" + this.getName() + ";年龄:" + this.getAge());
}
}
public class EncDemo05 {
public static void main(String[] args) {
Person per = new Person();
per.setName("张三");
per.setAge(30);
per.tell();
}
}
打印结果:
姓名:张三;年龄:30
3.构造方法与匿名对象
- 构造方法
对象的产生格式:
类名称 对象名称 = new 类名称();
因为有"()",所以表示的是一个方法,这实际上就是一个构造方法。
注:只要一有对象实例化,则就会调用构造方法
- 构造方法的定义格式
class 类名称 {
访问权限 类名称(类型1 参数1,类型2 参数2,...) {
程序语句;
... ... //构造方法没有返回值
}
}
在构造方法的声明中要牢记以下几点:
1.构造方法的名称必须与类名称一致
2.构造方法的声明处不能有任何返回值类型的声明
3.不能在构造方法中使用 return 返回一个值
- 声明一个构造方法
class Person {
public Person() { //声明构造方法
System.out.println("一个新的Person对象产生!");
}
}
public class ConsDemo01 {
public static void main(String[] args) {
System.out.println("声明对象:Person per = null;");
Person per = null; //声明对象时并不去调用构造方法
System.out.println("实例化对象:per = new Person();");
per = new Person(); //实例化对象
}
}
打印结果:
声明对象:Person per = null;
实例化对象:per = new Person();
一个新的Person对象产生!
Java 的操作机制:
在整个 Java 的操作中,如果一个类中没有明确的声明一个构造方法,则会自动生成一个无参的什么都不做的构造方法,供用户使用。
就类似于以下的形式:
class Person {
public Person() { //如果没有编写构造方法,则会自动生成此代码
}
}
构造方法的目的是为类中的属性初始化。既然是方法,则方法中肯定可以传递参数,此时定义一个构造,同时向里面传递参数
class Person {
private String name;
private int age;
public Person(String n,int a) { //声明构造方法,为类中的属性初始化
this.setName(n);
this.setAge(a);
}
public void setName(String n) {
name = n;
}
public void setAge(int a) {
if(a>=0&&a<=150) {
age = a;
}
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void tell() {
System.out.println("姓名:" + this.getName() + ",年龄:" + this.getAge());
}
}
public class ConsDemo02 {
public static void main(String[] args) {
Person per = new Person("张三",30);
per.tell();
}
}
打印结果:
姓名:张三,年龄:30
- 构造方法重载
构造方法本身与普通方法一样,都是支持重载操作的,只要参数的类型或个数不同,则就可以完成重载操作
class Person {
private String name;
private int age;
public Person(String n,int a) { //声明构造方法,为类中的属性初始化
this.setName(n);
this.setAge(a);
}
public Person(String n) { //声明有一个参数的构造方法
this.setName(n);
}
public Person() {} //声明一个无参的构造方法
public void setName(String n) {
name = n;
}
public void setAge(int a) {
if(a>=0&&a<=150) {
age = a;
}
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void tell() {
System.out.println("姓名:" + this.getName() + ",年龄:" + this.getAge());
}
}
public class ConsDemo03 {
public static void main(String[] args) {
Person per = new Person("张三",30);
per.tell();
}
}
打印结果:
姓名:张三,年龄:30
- 匿名对象(匿名:没有名字)
在 Java 中,如果一个对象只使用一次,则就可以将其定义成匿名对象
class Person {
private String name;
private int age;
public Person(String n,int a) { //声明构造方法,为类中的属性初始化
this.setName(n);
this.setAge(a);
}
public Person(String n) { //声明有一个参数的构造方法
this.setName(n);
}
public Person() {} //声明一个无参的构造方法
public void setName(String n) {
name = n;
}
public void setAge(int a) {
if(a>=0&&a<=150) {
age = a;
}
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void tell() {
System.out.println("姓名:" + this.getName() + ",年龄:" + this.getAge());
}
}
public class ConsDemo04 {
public static void main(String[] args) {
new Person("张三",30).tell();
}
}
打印结果:
姓名:张三,年龄:30
注:所谓的匿名对象就是比之前的对象少了一个栈内存的引用关系
4.实例讲解:类设计分析
程序分析思路:
1.根据要求写出类所包含的属性
2.所有的属性都必须进行封装(private)
3.封装之后的属性通过 setter 和 getter 设置和取得
4.如果需要可以加入若干构造方法
5.再根据其他要求添加相应的方法
6.类中的所有方法都不要直接输出,而是交给被调用处输出
题目要求
定义并测试一个名为 Student 的类,包括属性有“学号”,“姓名”,以及3门课程“数学”,“英语”和“计算机”的成绩,包括的方法有计算3门课程的“总分”,“平均分”,“最高分”及“最低分”
class Student {
private String stuNo;
private String stuName;
private float stuMath;
private float stuEnglish;
private float stuComputer;
public Student() {} //声明一个无参的构造方法
public Student(String s,String n,float m,float e,float c) { //声明构造方法,为类中的属性初始化
this.setStuNo(s);
this.setStuName(n);
this.setStuMath(m);
this.setStuEnglish(e);
this.setStuComputer(c);
}
public void setStuNo(String s) {
stuNo = s;
}
public void setStuName(String n) {
stuName = n;
}
public void setStuMath(float m) {
stuMath = m;
}
public void setStuEnglish(float e) {
stuEnglish = e;
}
public void setStuComputer(float c) {
stuComputer = c;
}
public String getStuNo() {
return stuNo;
}
public String getStuName() {
return stuName;
}
public float getStuMath() {
return stuMath;
}
public float getStuEnglish() {
return stuEnglish;
}
public float getStuComputer() {
return stuComputer;
}
public float sum() { //总分
return stuMath + stuEnglish + stuComputer;
}
public float avg() { //平均分
return this.sum()/3;
}
public float max() { //最高分
float max = stuMath;
max = max > stuEnglish ? max : stuEnglish;
max = max > stuComputer ? max : stuComputer;
return max;
}
public float min() { //最低分
float min = stuMath;
min = min < stuEnglish ? min : stuEnglish;
min = min < stuComputer ? min : stuComputer;
return min;
}
}
public class ExampleDemo01 {
public static void main(String[] args) {
Student stu = null;
stu = new Student("33","丁力",95.0f,90.0f,96.0f);
System.out.println("学生编号:" + stu.getStuNo());
System.out.println("学生姓名:" + stu.getStuName());
System.out.println("学生数学成绩:" + stu.getStuMath());
System.out.println("学生英语成绩:" + stu.getStuEnglish());
System.out.println("学生计算机成绩:" + stu.getStuComputer());
System.out.println("学生的总分:" + stu.sum());
System.out.println("学生的平均分:" + stu.avg());
System.out.println("学生的最高分:" + stu.max());
System.out.println("学生的最低分:" + stu.min());
}
}
打印结果:
学生编号:33
学生姓名:丁力
学生数学成绩:95.0
学生英语成绩:90.0
学生计算机成绩:96.0
学生的总分:281.0
学生的平均分:93.666664
学生的最高分:96.0
学生的最低分:90.0
5.String 类
- 实例化 String 对象
public class StringDemo01 {
public static void main(String[] args) {
String name = "DingLi"; //实例化String对象
System.out.println("姓名:" + name);
}
}
打印结果:
姓名:DingLi
public class StringDemo02 {
public static void main(String[] args) {
String name = new String("DingLi"); //实例化String对象
System.out.println("姓名:" + name);
}
}
打印结果:
姓名:DingLi
- String 的内容比较
基本数据类型,使用“==”进行数据的比较
范例:使用“==”进行比较
public class StringDemo03 {
public static void main(String[] args) {
int x = 30;
int y = 30;
System.out.println("两个数字的比较结果:" + (x==y));
}
}
打印结果:
两个数字的比较结果:true
public class StringDemo04 {
public static void main(String[] args) {
String str1 = "hello"; //直接赋值
String str2 = new String("hello"); //通过new赋值
String str3 = str2;
System.out.println("str1==str2-->" + (str1==str2));
System.out.println("str1==str3-->" + (str1==str3));
System.out.println("str2==str3-->" + (str2==str3));
}
}
打印结果:
str1==str2-->false
str1==str3-->false
str2==str3-->true
注:现在使用的“==”,判断的是数值,即栈内存空间的地址值
如果要想判断其内容是否相等,则就必须使用 String 类中提供的 equals() 方法完成。此方法使用如下:
public class StringDemo05 {
public static void main(String[] args) {
String str1 = "hello"; //直接赋值
String str2 = new String("hello"); //通过new赋值
String str3 = str2;
System.out.println("str1 equals str2-->" + (str1.equals(str2)));
System.out.println("str1 equals str3-->" + (str1.equals(str3)));
System.out.println("str2 equals str3-->" + (str2.equals(str3)));
}
}
打印结果:
str1 equals str2-->true
str1 equals str3-->true
str2 equals str3-->true
结论:String 有两种比较方式
.一种是使用“==”完成,比较的是地址值
.另一种是使用`equals`方法完成,比较的是具体的内容,开发中比较常用
- 两种实例化方式的区别
在 String 中可使用直接赋值和 new 调用构造方法的方式完成,那么使用哪种更合适呢?
一个字符串就是 String 的匿名对象
public class StringDemo06 {
public static void main(String[] args) {
System.out.println("\"hello\".equals(\"hello\") = " + "hello".equals("hello"));
}
}
打印结果:
"hello".equals("hello") = true
了解此处之后,实际上之前的操作:
String name = "DingLi";
就表示将一个堆内存空间的指向给了栈内存空间。
public class StringDemo07 {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hello";
String str3 = "hello";
System.out.println("str1==str2-->" + (str1==str2));
System.out.println("str1==str3-->" + (str1==str3));
System.out.println("str2==str3-->" + (str2==str3));
}
}
打印结果:
str1==str2-->true
str1==str3-->true
str2==str3-->true
以上三个对象的内存地址都一样。
内存分析
注:使用直接赋值的方式,可以有效的节省内存
public class StringDemo08 {
public static void main(String[] args) {
String str1 = new String("hello");
}
}
开辟了两个空间,浪费资源
使用直接赋值的方式只需要一个实例化对象即可,而使用 new String() 的方式则意味着要开辟两个内存对象。开发中最好使用直接赋值的方式完成。
- 字符串的内容不可改变(重要特征)
谨记:字符串对象改变了,字符串没变
public class StringDemo09 {
public static void main(String[] args) {
String str = "hello";
str = str + "\0world!!!";
System.out.println("str = " + str);
}
}
打印结果:
str = hello world!!!
注:JVM 其实把str = str + "\0world!!!";
看作一个整体进行操作,是一个对象,即hello world!!!
对象。
注:实际上字符串内容没改变,改变的是内存地址的引用关系。
在开发中,避免如下使用操作:
String str1 = "DingLi";
for(int i=0;i<100;i++) {
str1 += i;
}
System.out.println(str1);
但是这样的操作下,要断开连接引用100次才可以完成。这样的操作性能很低,应该避免使用,而如果非要使用这种操作,在后面的 Java 常用类库中将介绍 StringBuffer 类,专门完成这样的功能。
6.String 类的常用方法
split() 和 replaceAll() 这两个方法是需要正则支持的,关于正则的应用。
- 字符数组与字符串
一个字符串可变为一个字符数组,同样,也可把一个字符数组,变为一个字符串
在 String 类中提供了以下的操作方法:
.将字符串变为字符数组:public char[] toCharArray()
.字符数组变为字符串:
1.public String(char[] value)
2.public String(char[] value,int offset,int count)
public class StringAPIDemo01 {
public static void main(String[] args) {
String str1 = "hello"; //字符串定义
char c[] = str1.toCharArray(); //将一个字符串变为字符数组
for(int i=0;i<c.length;i++) {
System.out.print(c[i] + "、");
}
System.out.println("");
String str2 = new String(c); //将全部的字符数组变为String
String str3 = new String(c,0,3); //将部分字符数组变为String
System.out.println(str2);
System.out.println(str3);
}
}
打印结果:
h、e、l、l、o、
hello
hel
- 从字符串中取出指定位置的字符
public char charAt(int index)
public class StringAPIDemo02 {
public static void main(String[] args) {
String str1 = "hello"; //字符串定义
System.out.println(str1.charAt(3)); //取出字符串中第四个字符
}
}
打印结果:
l
- 字符串与 byte 数组的转换
byte 数组(字节数组),在一般的 IO 操作中会经常使用到
字符串与字节数组间的转换
.字符串变为字节数组:
public byte[] getBytes()
.将一个字节数组变为字符串:
1.将全部字节数组变为String:
public String(byte[] bytes)
2.将部分字节数组变为String:
public String(byte[] bytes,int offset,int length)
public class StringAPIDemo03 {
public static void main(String[] args) {
String str1 = "hello"; //字符串定义
byte[] b = str1.getBytes();
System.out.println(new String(b));
System.out.println(new String(b,1,3));
}
}
打印结果:
hello
ell
- 取得一个字符串的长度
要想取得字符串中的长度:public int length()
public class StringAPIDemo04 {
public static void main(String[] args) {
String str1 = "hello DingLi";
System.out.println("\"" + str1 + "\"的长度为:" + str1.length());
}
}
打印结果:
"hello DingLi"的长度为:12
- 查找指定的字符串是否存在
在实际操作中,经常会使用到判断一个字符串中是否存在某些内容,此时就可以使用以下的方法:
.从头开始查找:public int indexOf(String str)
.从指定位置开始查找:public int indexOf(String str,int fromIndex)
返回值是一个 int 类型的数据,若有,则返回一个字符串的具体位置,若没有,则返回“-1”。
public class StringAPIDemo05 {
public static void main(String[] args) {
String str1 = "abcdefgcgh";
System.out.println(str1.indexOf("c")); //全部查找
System.out.println(str1.indexOf("c",3)); //从第四个位置开始查
System.out.println(str1.indexOf("x")); //没有查找到,返回-1
}
}
打印结果:
2
7
-1
- 去掉空格
如果假设一些信息是由用户输入的话,则就有可能出现多余的空格,在这种操作中就可以使用 trim() 去掉字符串的左右空格,但是字符串中间的空格是不可能去掉的。
public class StringAPIDemo06 {
public static void main(String[] args) {
String str1 = " hello ";
System.out.println(str1.trim()); //去掉左右空格后输出
}
}
打印结果:
hello
- 字符截取
从一个指定的字符串中取出里面的部分内容,使用的方法:
.从指定位置开始一直截取到结束位置:
public String substring(int beginIndex)
.截取指定范围的字符串:
public String substring(int beginIndex,int endIndex)
该子字符串从指定的 beginIndex 处开始,直到索引 endIndex-1 处的字符。因此,该子字符串的长度为 endIndex-beginIndex。
public class StringAPIDemo07 {
public static void main(String[] args) {
String str1 = "helloworld!!!";
System.out.println(str1.substring(6)); //从第7个位置一直截取到最后
System.out.println(str1.substring(0,5)); //从第0个位置开始截取,一直截取到第4个位置
}
}
打印结果:
orld!!!
hello
- 拆分字符串( regex 为正则表达式)
如果现在需要按指定的字符串去拆分一个字符串的话,则使用:public String[] split(String regex)
public class StringAPIDemo08 {
public static void main(String[] args) {
String str1 = "hello world";
String s[] = str1.split(" "); //按空格进行字符串的拆分
for(int i=0;i<s.length;i++) {
System.out.println(s[i]);
}
}
}
打印结果:
hello
world
- 大小写转换
此功能在一般的开发语言都会存在,将一个大写的字符串全部字母变为小写或者将一个小写的字符串中的全部字母变为大写:
.小写变大写:public String toUpperCase()
.大写变小写:public String toLowerCase()
public class StringAPIDemo09 {
public static void main(String[] args) {
System.out.println("将\"hello world\"转成大写:" + "hello world".toUpperCase());
System.out.println("将\"HELLO WORLD\"转成小写:" + "HELLO WORLD".toLowerCase());
}
}
打印结果:
将"hello world"转成大写:HELLO WORLD
将"HELLO WORLD"转成小写:hello world
判断是否以指定的字符串开头或者结尾
判断是否以指定的字符串开头:
public boolean startsWith(String prefix)
判断是否以指定的字符串结尾
public boolean endsWith(String suffix)
public class StringAPIDemo10 {
public static void main(String[] args) {
String str1 = "**HELLO";
String str2 = "HELLO**";
if(str1.startsWith("**")) {
System.out.println("(**HELLO)以**开头!");
}
if(str2.endsWith("**")) {
System.out.println("(HELLO**)以**结尾!");
}
}
}
打印结果:
(**HELLO)以**开头!
(HELLO**)以**结尾!
- 不区分大小写的比较
在 String 类中 equals() 可用来进行字符串比较,仅限于字符串大小写完全一样。大小写不区分使用:public boolean equalsIgnoreCase(String anotherString)
public class StringAPIDemo11 {
public static void main(String[] args) {
String str1 = "HELLO";
String str2 = "hello";
System.out.println("\"HELLO\" equals \"hello\" = " + str1.equals(str2));
System.out.println("\"HELLO\" equalsIgnoreCase \"hello\" = " + str1.equalsIgnoreCase(str2));
}
}
打印结果:
"HELLO" equals "hello" = false
"HELLO" equalsIgnoreCase "hello" = true
- 字符串替换功能( regex 为正则表达式)
用于字符串替换的操作:public String replaceAll(String regex,String replacement)
public class StringAPIDemo12 {
public static void main(String[] args) {
String str = "hello";
String newStr = str.replaceAll("l","x");
System.out.println("替换之后的结果:" + newStr);
}
}
打印结果:
替换之后的结果:hexxo
7.引用传递及基本应用
三道引用传递范例
- 引用传递(1)
class Demo {
int temp = 30;
}
public class RefDemo01 {
public static void main(String[] args) {
Demo d1 = new Demo();
d1.temp = 50; //修改temp属性的内容
System.out.println("fun()方法调用之前:" + d1.temp);
fun(d1);
System.out.println("fun()方法调用之后:" + d1.temp);
}
public static void fun(Demo d2) { //此处的方法由主方法直接调用
d2.temp = 1000; //修改temp值
}
}
打印结果:
fun()方法调用之前:50
fun()方法调用之后:1000
- 引用传递(1)——内存分析
- 引用传递(2)
public class RefDemo02 {
public static void main(String[] args) {
String str1 = "hello";
System.out.println("fun()方法调用之前:str1 = " + str1);
fun(str1);
System.out.println("fun()方法调用之后:str1 = " + str1);
}
public static void fun(String str2) {
str2 = "DingLi";
}
}
打印结果:
fun()方法调用之前:str1 = hello
fun()方法调用之后:str1 = hello
- 引用传递(2)——内存分析
映射:多对一,不能一对多
- 引用传递(3)
class Demo{
String temp = "hello";
}
public class RefDemo03 {
public static void main(String args[]){
Demo d1 = new Demo() ; // 实例化Demo对象
d1.temp = "world"; // 修改temp属性的内容
System.out.println("fun()方法调用之前:" + d1.temp) ;
fun(d1) ;
System.out.println("fun()方法调用之后:" + d1.temp) ;
}
public static void fun(Demo d2){ // 此处的方法由主方法直接调用
d2.temp = "DingLi"; // 修改temp值
}
}
打印结果:
fun()方法调用之前:world
fun()方法调用之后:DingLi
- 引用传递(3)——内存分析
注:String 是一个特殊的类,其内容不可改变。而类属性可以随意修改。
- 接受本类引用
class Demo{
private int temp = 30;
public void setTemp(int t) {
temp = t;
}
public int getTemp() {
return temp;
}
}
public class RefDemo04 {
public static void main(String args[]){
Demo d1 = new Demo() ; //实例化Demo对象
d1.setTemp(50); //修改temp属性的内容
d1.temp = 30;
System.out.println("d1.temp = " + d1.temp);
}
}
打印结果:
RefDemo04.java:14: 错误: temp可以在Demo中访问private
d1.temp = 30;
^
RefDemo04.java:15: 错误: temp可以在Demo中访问private
System.out.println("d1.temp = " + d1.temp);
^
2 个错误
因为此时 d1 是在 Demo 类的外部,所以无法访问,那么如果现在按照引用传递的方式将其传入到其内部呢?就相当于自己接受自己本类的引用对象。
class Demo{
private int temp = 30;
public void setTemp(int t) {
temp = t;
}
public int getTemp() {
return temp;
}
public void fun(Demo d2) {
d2.temp = 50; //直接通过对象调用本类的私有属性
}
}
public class RefDemo04 {
public static void main(String args[]){
Demo d1 = new Demo() ; //实例化Demo对象
d1.fun(d1); //此处把Demo的对象传回到自己的类中
System.out.println("temp = " + d1.getTemp());
}
}
打印结果:
temp = 50
注:只要是符合了引用传递的语法,则可以向任意的地方传递。
- 范例讲解:一对一关系
引用传递可表示出生活中的以下场景:一个人有一本书,一本书属于一个人
class Person{
private String name;
private int age;
private Book book; //一个人有一本书
public Person() {}
public Person(String name,int age) {
this.setName(name);
this.setAge(age);
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setBook(Book book) {
this.book = book;
}
public Book getBook() {
return book;
}
}
class Book{
private String title;
private float price;
private Person person;
//添加两个构造方法
public Book() {}
public Book(String title,float price) {
this.setTitle(title);
this.setPrice(price);
}
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setPrice(float price) {
this.price = price;
}
public float getPrice() {
return price;
}
public void setPerson(Person person) {
this.person = person;
}
public Person getPerson() {
return person;
}
}
public class RefDemo05 {
public static void main(String args[]){
Person per = new Person("张三",30);
Book bk = new Book("Java编程思想",90.0f);
per.setBook(bk); //一个人有一本书
bk.setPerson(per); //一本书属于一个人
System.out.println("从人找到书——>姓名:" + per.getName() + ";年龄:" + per.getAge() + ";书名:" + per.getBook().getTitle() + ";价格:" + per.getBook().getPrice());
System.out.println("从书找到人——>书名:" + bk.getTitle() + ";价格:" + bk.getPrice() + ";姓名:" + bk.getPerson().getName() + ";年龄:" + bk.getPerson().getAge());
}
}
打印结果:
从人找到书——>姓名:张三;年龄:30;书名:Java编程思想;价格:90.0
从书找到人——>书名:Java编程思想;价格:90.0;姓名:张三;年龄:30
- 范例讲解:进一步深入一对一关系
一个人有一个孩子,孩子也有一本书
class Person{
private String name;
private int age;
private Book book; //一个人有一本书
private Person child; //一个人有一个孩子
public Person() {}
public Person(String name,int age) {
this.setName(name);
this.setAge(age);
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setBook(Book book) {
this.book = book;
}
public Book getBook() {
return book;
}
public void setChild(Person child) {
this.child = child;
}
public Person getChild() {
return child;
}
}
class Book{
private String title;
private float price;
private Person person;
//添加两个构造方法
public Book() {}
public Book(String title,float price) {
this.setTitle(title);
this.setPrice(price);
}
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setPrice(float price) {
this.price = price;
}
public float getPrice() {
return price;
}
public void setPerson(Person person) {
this.person = person;
}
public Person getPerson() {
return person;
}
}
public class RefDemo06 {
public static void main(String args[]){
Person per = new Person("张三",30);
Book bk = new Book("Java编程思想",90.0f);
per.setBook(bk); //一个人有一本书
bk.setPerson(per); //一本书属于一个人
Person cld = new Person("张草",10);
Book b = new Book("一千零一夜",30.3f);
cld.setBook(b); //一个孩子有一本书
b.setPerson(cld); //一本书属于一个孩子
per.setChild(cld); //一个人有一个孩子
//通过人找到孩子,并找到孩子所拥有的书
System.out.println(per.getName() + "的孩子——>姓名:" + per.getChild().getName() + ";年龄:" + per.getChild().getAge() + ";书名:" + per.getChild().getBook().getTitle() + ";价格:" + per.getChild().getBook().getPrice());
}
}
打印结果:
张三的孩子——>姓名:张草;年龄:10;书名:一千零一夜;价格:30.3
8.this 关键字
this 关键字的作用
- 强调本类中的方法
- 表示本类中的属性
- 使用 this 调用本类的构造方法
- this 表示当前对象
表示本类中的属性
class Person{
private String name;
private int age;
public Person(String n,int a) { //通过构造方法赋值
name = n;
age = a;
}
public String getInfo() {
return "姓名:" + name + ",年龄:" +age;
}
}
从 n 或 a 中并不能明确的发现参数表示的具体含义。
代码改进:
class Person{
private String name;
private int age;
public Person(String name,int age) { //通过构造方法赋值
name = name;
age = age;
}
public String getInfo() {
return "姓名:" + name + ",年龄:" +age;
}
}
public class ThisDemo01 {
public static void main(String[] args) {
Person per1 = new Person("张三",33);
System.out.println(per1.getInfo()); //取得信息
}
}
打印结果:
姓名:null,年龄:0
此时,并没有正确的将内容赋值给属性。此时操作的 name 和 age 实际上都是构造方法中的,跟类中的属性完全不沾边。(就近原则)
为了明确表示出类中的属性,要加上”this.属性名称”的操作:
class Person{
private String name;
private int age;
public Person(String name,int age) { //通过构造方法赋值
this.name = name;
this.age = age;
}
public String getInfo() {
return "姓名:" + name + ",年龄:" +age;
}
}
public class ThisDemo02 {
public static void main(String[] args) {
Person per1 = new Person("张三",33);
System.out.println(per1.getInfo()); //取得信息
}
}
打印结果:
姓名:张三,年龄:33
- 使用 this 调用构造方法
如果一个类中有多个构造方法的话,也可以利用 this 关键字互相调用。
class Person{
private String name;
private int age;
//构造方法
public Person() {
System.out.println("新对象实例化");
}
public Person(String name) {
System.out.println("新对象实例化");
this.name = name;
}
public Person(String name,int age) { //通过构造方法赋值
System.out.println("新对象实例化");
this.name = name;
this.age = age;
}
public String getInfo() {
return "姓名:" + name + ",年龄:" +age;
}
}
public class ThisDemo03 {
public static void main(String[] args) {
Person per1 = new Person("张三",33);
System.out.println(per1.getInfo()); //取得信息
}
}
代码冗余,使用 this 修改以上的代码
this (若干参数)
class Person{
private String name;
private int age;
//构造方法
public Person() {
System.out.println("新对象实例化");
}
public Person(String name) {
this(); //调用本类中的无参构造方法
this.name = name;
}
public Person(String name,int age) { //通过构造方法赋值
this(name); //调用本类中有一个参数的构造方法
this.age = age;
}
public String getInfo() {
return "姓名:" + name + ",年龄:" +age;
}
}
public class ThisDemo04 {
public static void main(String[] args) {
Person per1 = new Person("张三",33);
System.out.println(per1.getInfo()); //取得信息
}
}
注:在使用 this 关键字调用其他构造方法的时候,有以下几点限制:
1.this() 调用构造方法的语句只能放在构造方法的首行
2.在使用 this 调用本类中其他构造的时候,至少有一个构造方法是不用 this 调用的
两个错误案例
案例一:
class Person{
private String name;
private int age;
//构造方法
public Person() {
System.out.println("新对象实例化");
}
public Person(String name) {
this.name = name;
this(); //调用本类中的无参构造方法
}
public Person(String name,int age) { //通过构造方法赋值
this(name); //调用本类中有一个参数的构造方法
this.age = age;
}
public String getInfo() {
return "姓名:" + name + ",年龄:" +age;
}
}
public class ThisDemo05 {
public static void main(String[] args) {
Person per1 = new Person("张三",33);
System.out.println(per1.getInfo()); //取得信息
}
}
打印结果:
ThisDemo05.java:10: 错误: 对this的调用必须是构造器中的第一个语句
this(); //调用本类中的无参构造方法
^
1 个错误
案例二:
class Person{
private String name;
private int age;
//构造方法
public Person() {
this(name,age); //调用本类中两个参数的构造方法
System.out.println("新对象实例化");
}
public Person(String name) {
this(); //调用本类中的无参构造方法
this.name = name;
}
public Person(String name,int age) { //通过构造方法赋值
this(name); //调用本类中有一个参数的构造方法
this.age = age;
}
public String getInfo() {
return "姓名:" + name + ",年龄:" +age;
}
}
public class ThisDemo05 {
public static void main(String[] args) {
Person per1 = new Person("张三",33);
System.out.println(per1.getInfo()); //取得信息
}
}
打印结果:
ThisDemo05.java:6: 错误: 无法在调用超类型构造器之前引用name
this(name,age); //调用本类中两个参数的构造方法
^
ThisDemo05.java:6: 错误: 无法在调用超类型构造器之前引用age
this(name,age); //调用本类中两个参数的构造方法
^
ThisDemo05.java:5: 错误: 递归构造器调用
public Person() {
^
3 个错误
注:构造方法出现了递归调用,此时是一个错误的程序
- 使用 this 表示当前对象
当前对象:当前正在调用方法的对象。
class Person{
public String getInfo() {
System.out.println("Person类——>" + this); //直接打印this
return null;
}
}
public class ThisDemo06 {
public static void main(String[] args) {
Person per1 = new Person();
Person per2 = new Person();
System.out.println("MAIN方法——>" + per1); //直接打印对象
per1.getInfo(); //当前调用getInfo()方法的对象是per1
System.out.println("MAIN方法——>" + per2); //直接打印对象
per2.getInfo(); //当前调用getInfo()方法的对象是per2
}
}
打印结果:
MAIN方法——>Person@15db9742
Person类——>Person@15db9742
MAIN方法——>Person@6d06d69c
Person类——>Person@6d06d69c
- 对象比较
可以使用 this 和引用传递进行两个对象是否相等的判断。
class Person{
private String name;
private int age;
public Person(String name,int age) {
this.setName(name);
this.setAge(age);
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
}
生成两个对象,当两个对象中的姓名和年龄完全相等的时候,则认为对象是相等的。但此时产生两个问题:
1.如何进行对象的比较?
2.在哪块进行对象的比较?
范例:对象比较的一种方式
class Person{
private String name;
private int age;
public Person(String name,int age) {
this.setName(name);
this.setAge(age);
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
}
public class ThisDemo07 {
public static void main(String[] args) {
Person per1 = new Person("张三",30); //声明两个对象,内容完全相等
Person per2 = new Person("张三",30);
//直接在主方法中依次取得各个属性进行比较
if(per1.getName().equals(per2.getName())&&per1.getAge()==per2.getAge()) {
System.out.println("两个对象相等!");
}else {
System.out.println("两个对象不相等!");
}
}
}
打印结果:
两个对象相等!
应该由自己进行比较最合适,所以应该在 Person 类中增加一个比较的方法。
class Person{
private String name;
private int age;
public Person(String name,int age) {
this.setName(name);
this.setAge(age);
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
public boolean compare(Person per) {
//调用此方法时里面存在两个对象:当前对象和传入的对象
Person p1 = this; //当前的对象,就表示per1
Person p2 = per; //传递进来的对象,就表示per2
if(p1==p2) { //判断是不是同一个对象,用地址比较
return true;
}
//之后分别判断每一个属性是否相等
if(p1.name.equals(p2.name)&&p1.age==p2.age) {
return true;
}else {
return false;
}
}
}
public class ThisDemo08 {
public static void main(String[] args) {
Person per1 = new Person("张三",30); //声明两个对象,内容完全相等
Person per2 = new Person("张三",30);
if(per1.compare(per2)) {
System.out.println("两个对象相等!");
}else {
System.out.println("两个对象不相等!");
}
}
}
打印结果:
两个对象相等!
注:地址相等,则两个对象相等;若地址不相等,则依次判断每一个属性的内容来判断其是否相等。
9.static 关键字
在 Java 中可使用 static 声明属性或方法,因为之前所讲解的都属于非 static 的,每个对象都占有各自的内容。希望一个属性被所有对象所共同拥有,则可以将其声明为 static 类型,声明之后,可以由类名称直接调用。
- 使用 static 声明属性
static 可声明全局属性,其作用呢?
class Person {
String name;
int age;
String country = "A城";
public Person(String name,int age) {
this.name = name;
this.age = age;
}
public void info() {
System.out.println("姓名:" + this.name + ",年龄:" + this.age + ",城市:" + this.country);
}
}
public class StaticDemo01{
public static void main(String[] args) {
Person p1 = new Person("张三",30);
Person p2 = new Person("李四",31);
Person p3 = new Person("王五",32);
p1.info();
p2.info();
p3.info();
}
}
打印结果:
姓名:张三,年龄:30,城市:A城
姓名:李四,年龄:31,城市:A城
姓名:王五,年龄:32,城市:A城
如果此时城市的名称不叫“A城”而改成了“B城”,如果现在已经产生了 5000 个对象,那么意味着,此时需修改 5000 个对象中的 country 属性。所以,此时最好使用 static 关键字声明属性。
范例:使用 static 修改城市属性
class Person {
String name;
int age;
static String country = "A城";
public Person(String name,int age) {
this.name = name;
this.age = age;
}
public void info() {
System.out.println("姓名:" + this.name + ",年龄:" + this.age + ",城市:" + this.country);
}
}
public class StaticDemo02{
public static void main(String[] args) {
Person p1 = new Person("张三",30);
Person p2 = new Person("李四",31);
Person p3 = new Person("王五",32);
System.out.println("——————修改之前——————");
p1.info();
p2.info();
p3.info();
p1.country = "B城"; //修改static属性
System.out.println("——————修改之后——————");
p1.info();
p2.info();
p3.info();
}
}
打印结果:
——————修改之前——————
姓名:张三,年龄:30,城市:A城
姓名:李四,年龄:31,城市:A城
姓名:王五,年龄:32,城市:A城
——————修改之后——————
姓名:张三,年龄:30,城市:B城
姓名:李四,年龄:31,城市:B城
姓名:王五,年龄:32,城市:B城
修改一个对象中的 country 属性,则其他对象的 country 属性内容全部改变,证明此属性是所有对象共享的。
- 内存关系图
每一个对象都拥有各自的堆栈空间,堆内存空间中保存每个对象各自的属性,但是所有的 static 属性是保存在全局数据区之中,所有的对象指向全局数据区中的一个内容,所以当一个对象修改之后,所有对象的内容将全部变化。
Java 中的内存区域:
栈内存:可保存对象的引用,即堆内存的地址
堆内存:保存每个对象的具体属性
全局数据区:保存 static 类型的属性
全局代码区:保存所有方法的定义
一般在调用 static 属性的时候最好是使用类名称直接调用,采用“类名称.属性”的形式调用。
class Person {
String name;
int age;
static String country = "A城";
public Person(String name,int age) {
this.name = name;
this.age = age;
}
public void info() {
System.out.println("姓名:" + this.name + ",年龄:" + this.age + ",城市:" + this.country);
}
}
public class StaticDemo03{
public static void main(String[] args) {
Person p1 = new Person("张三",30);
Person p2 = new Person("李四",31);
Person p3 = new Person("王五",32);
System.out.println("——————修改之前——————");
p1.info();
p2.info();
p3.info();
//p1.country = "B城"; //修改static属性
Person.country = "B城";
System.out.println("——————修改之后——————");
p1.info();
p2.info();
p3.info();
}
}
打印结果:
——————修改之前——————
姓名:张三,年龄:30,城市:A城
姓名:李四,年龄:31,城市:A城
姓名:王五,年龄:32,城市:A城
——————修改之后——————
姓名:张三,年龄:30,城市:B城
姓名:李四,年龄:31,城市:B城
姓名:王五,年龄:32,城市:B城
- 声明 static 方法
如果一个方法使用了 static 关键字声明,则此方法可以直接使用类名称进行调用,下面将之前代码中的全部属性进行封装。
class Person {
private String name;
private int age;
private static String country = "A城";
public static void setCountry(String c) { //此方法可直接由类名称调用
country = c;
}
public Person(String name,int age) {
this.name = name;
this.age = age;
}
public void info() {
System.out.println("姓名:" + this.name + ",年龄:" + this.age + ",城市:" + this.country);
}
}
public class StaticDemo04{
public static void main(String[] args) {
Person p1 = new Person("张三",30);
Person p2 = new Person("李四",31);
Person p3 = new Person("王五",32);
System.out.println("——————修改之前——————");
p1.info();
p2.info();
p3.info();
//p1.country = "B城"; //修改static属性
//Person.country = "B城";
Person.setCountry("B城"); //调用静态方法修改static属性的内容
System.out.println("——————修改之后——————");
p1.info();
p2.info();
p3.info();
}
}
打印结果:
——————修改之前——————
姓名:张三,年龄:30,城市:A城
姓名:李四,年龄:31,城市:A城
姓名:王五,年龄:32,城市:A城
——————修改之后——————
姓名:张三,年龄:30,城市:B城
姓名:李四,年龄:31,城市:B城
姓名:王五,年龄:32,城市:B城
注:使用 static 方法,不能调用非 static 的属性或方法。
class Person {
private static String country = "A城";
private String name = "Hello";
public static void sFun(String c) {
System.out.println("name = " + name); //错误,不能调用非static属性
fun(); //错误,不能调用非static方法
}
public void fun() {
System.out.println("World");
}
}
打印结果:
StaticDemo05.java:5: 错误: 无法从静态上下文中引用非静态 变量 name
System.out.println("name = " + name); //错误,不能调用
非static属性
^
StaticDemo05.java:6: 错误: 无法从静态上下文中引用非静态 方法 fun()
fun(); //错误,不能调用非static方法
^
2 个错误
直接编译看结果。
原因:因为 static 属性或者方法可以在对象没有实例化的时候就可直接调用使用了。
class Person {
String name;
int age;
static String country = "A城";
public Person(String name,int age) {
this.name = name;
this.age = age;
}
public void info() {
System.out.println("姓名:" + this.name + ",年龄:" + this.age + ",城市:" + this.country);
}
}
public class StaticDemo03{
public static void main(String[] args) {
Person.country = "B城";
Person p1 = new Person("张三",30);
Person p2 = new Person("李四",31);
Person p3 = new Person("王五",32);
System.out.println("——————修改之前——————");
p1.info();
p2.info();
p3.info();
//p1.country = "B城"; //修改static属性
System.out.println("——————修改之后——————");
p1.info();
p2.info();
p3.info();
}
}
打印结果:
——————修改之前——————
姓名:张三,年龄:30,城市:B城
姓名:李四,年龄:31,城市:B城
姓名:王五,年龄:32,城市:B城
——————修改之后——————
姓名:张三,年龄:30,城市:B城
姓名:李四,年龄:31,城市:B城
姓名:王五,年龄:32,城市:B城
- static 的其他应用
因为每个实例化对象在操作的时候都必须调用构造方法完成,所以如果现在要进行统计的话,则直接在构造方法之中增加一个 static 的属性即可。
class Demo {
private static int count = 0; //所有对象共享此属性
public Demo() {
count++; //只要有对象产生就应该自增
System.out.println("产生了" + count + "个对象!");
}
}
public class StaticDemo06 {
public static void main(String[] args) {
new Demo();
new Demo();
new Demo();
}
}
打印结果:
产生了1个对象!
产生了2个对象!
产生了3个对象!
使用 static 为对象进行自动的编名操作,此操作与上面代码类似。
class Demo {
private String name;
private static int count = 0; //所有对象共享此属性
public Demo() {
count++; //只要有对象产生就应该自增
this.name = "DEMO-" + count; //自动进行编名操作
}
public Demo(String name) {
this.name = name;
}
public String getName() { //取得姓名
return this.name;
}
}
public class StaticDemo07 {
public static void main(String[] args) {
System.out.println(new Demo().getName());
System.out.println(new Demo("DingLi").getName());
System.out.println(new Demo().getName());
System.out.println(new Demo("Software Engineer").getName());
System.out.println(new Demo().getName());
}
}
打印结果:
DEMO-1
DingLi
DEMO-2
Software Engineer
DEMO-3