Java 数学学习手册(三)
二十四、面向对象编程中的类
一个对象本质上是一个事物的表示。一个物体有一些属性,就像任何事物都有特性一样。然而,编程世界中的类主要是为特定对象设计的数据结构。这个类也被认为是一个对象的蓝图。它跟踪关于该对象的一系列相关事物。这些相关的东西被称为字段、属性和函数(或方法)。
字段是类的数据成员。在使用它们之前,必须声明和初始化它们。它们主要供班级内部使用。
一些字段可以充当属性,即对象的属性(例如,雇员的姓名或银行账户的余额)。
属性可以被 setters 改变,也可以被 getters 从类外部访问。
Getters 和 setters 是隐藏类属性内部实现的方法。这种设计使开发人员能够在以后轻松地更新一些现有的实现。它是面向对象编程的封装特性的一个例子。
在下面的示例类Student中,
-
firstName、lastName、age都是字段。因为所有这些字段都有 getter/setter,例如,getAge(),setAge(),所以它们也是属性。 -
public Student()是在Student类中定义的默认构造函数。它就像一个方法,当从Student类创建对象时执行,即:Student student = new Student(); -
public Student(String firstName, String lastName)是Student类的另一个构造函数。它通过直接将firstName和lastName分配给字段来实例化(或创建)一个对象,即:Student student = new Student("John", "Doe"); -
public String getFirstName()、public String getLastName()、public int getAge()是查询私有字段firstName、lastName、age的值的方法。他们是吸气剂。 -
public void setAge(int age)是给私有字段age赋值的方法。它是二传手。 -
数据类型前面的关键字
public和private(如String、int)或方法名称为访问修饰符。public意味着它对所有人都可见,而private只对当前的类范围开放。public class Student { private String firstName; private String lastName; private int age; public Student() { } public Student(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } public int getAge() { return this.age; } public void setAge(int age) { this.age = age ; } }一个简单的结构视图是:
class Student { //fields, e.g. firstName, lastName, and age //constructor //setter/getter methods, e.g. get/set firstName, lastName, and age }
Student类中的关键字this是对当前对象的引用,其字段(如firstName、lastName、age)正在被使用。通过使用this,也可以调用当前对象的方法或构造函数。
程序中有两种所谓的非字段。
-
方法中的局部变量
-
参数
x1和x2的方法如下:myMethod(x1, x2)
类和对象之间的关系:
-
不上课可以有对象吗?不
-
没有对象可以上课吗?是
-
我可以创建一个类的多个实例吗?是
实验室工作
创建一个名为Name的类来表示一个人的名字。这个类应该有名为firstName的字段代表这个人的名,lastName代表他们的姓,middleInitial代表他们的中间名首字母(单个字符)。目前,您的类应该只包含字段。
实验室工作
创建一个名为Vehicle的公共类的大纲。
public class Vehicle {
......
}
然后创建一个主程序来操作创建的Vehicle对象。
实验室工作
向下面显示的Point类添加一个构造函数,它接受另一个Point作为参数,并将新的Point初始化为具有相同的(x,y)值。在您的解决方案中使用关键字this。
然后,将名为setX和setY的方法添加到Point类。每个方法都接受一个整数参数,并将Point对象的 x 或 y 坐标分别更改为传递的值。
public class Point {
int x;
int y;
// your code goes here
}
问题
-
一个对象和一个类有什么不同?
-
对象用于面向对象的编程,类用于面向类的编程。
-
对象是封装相关数据和行为的实体,而类是一类对象的蓝图。
-
一个对象不封装,一个类封装,使得类比对象更强大,更可重用。
-
对象是一种不包含任何行为的类。
-
类是对象的实例。一个对象可以用来创建许多类。
-
-
描述一个关于类和对象的概念是如何被使用的真实场景。
-
设计并编写一个名为
Game的类。你需要考虑这个类应该有什么样的字段,然后添加几个方法来丰富这个类。For example,
-
你可以定义游戏的价格;
-
你可以将游戏分为“电脑游戏”或“电子游戏”;
-
你可以定义游戏可以使用的平台,例如“xbox”、“playstation”、“nintendo”等。;
-
您可以用参数定义构造函数;
-
您可以定义方法来设置/获取上面提到的任何字段;
-
当你想把一个
Game定义为一个类时,你能想到的任何东西。
-
-
给定下面这个名为
NumberHolder的类,编写一些代码来创建该类的一个实例,初始化它的两个成员变量,然后显示每个成员变量的值。public class NumberHolder { public int anInt; public float aFloat; } -
以下哪些是字段和参数之间的区别?这个问题可能有多个答案。
-
字段是存在于对象内部的变量,而参数是方法内部的变量,其值是从外部传入的。
-
字段可以存储许多值,而参数只能存储一个值。字段语法不同,因为它们可以用关键字
private声明。 -
参数必须是值的基本类型,而字段可以是对象。
-
字段的范围是整个类,而参数的范围仅限于方法。
-
在计算机中,字段比参数占用更多的内存。
-
每个类只能有一个字段,但是可以有任意多个参数。
-
字段是常量,只能设置一次,而参数在每次调用时都会改变。
-
-
假设
Account类中的方法被定义为:public double computeInterest(int rate)And suppose the client code has declared an
Accountvariable namedacct. Which of the following would be a valid call to the above method?-
int result = Account.computeInterest(14); -
double result = acct.computeInterest(14); -
double result = computeInterest(acct, 14); -
new Account(14).computeInterest(); -
acct.computeInterest(14, 15);
-
二十五、接口——整体抽象
接口的概念是抽象的一部分,是四个面向对象的概念之一。特点。抽象是关于公共特性的抽象设计,包括对象的操作。
接口是一个类的蓝图。但是,它既不是类,也不是对象。接口中定义的所有方法都是抽象的。接口的任何方法中都不允许有实现细节。将要实现接口的类将负责方法的实际实现。
让我们看一个接口和从它实现的类的例子。Auto是代表车辆的通称。我们使用Auto来定义一个接口。
public interface Auto {
void start();
void stop();
void turn();
void back();
void park();
}
作为汽车的两种常见类型,轿车和公共汽车是常见的对象。轿车和公共汽车共享在Auto接口中定义的相同类型的行为。当我们为汽车和公共汽车创建一个类时,我们使用关键字implements从相同的Auto接口用不同的行为细节实现汽车和公共汽车。
我们使用类Car作为例子:
public class Car implements Auto {
private String maker;
public void start() {
// car starts its engine
}
public void stop() {
// car stops its engine
}
public void turn() {
// car turns left or right at a corner
}
public void back() {
// car backs
}
public void park() {
// car parks
}
public String getMaker() {
return this.maker;
}
public void setMaker(String maker) {
this.maker = maker;
}
}
你可能已经注意到了,
-
一个接口指示对象能做什么。
-
当一个类实现接口时,它用必要的细节定义对象正在做什么。
接口的设计允许开发人员在不改变调用者实现的情况下修改底层类;这有时被称为接口编码。至少在两种情况下我们应该考虑采用接口设计。
-
当我们只想指定特定数据类型的行为,而不关心谁实现了它的行为时。
例如:
我们定义了一个接口
Auto,它是一个抽象概念和通用术语。在这个接口中,我们通过签名定义了几个方法,如start、stop、turn、back、park,没有任何实现细节。当我们创建实现Auto接口的Car或Truck类时,我们将在这些方法中添加实现细节。 -
当所有的类都有相同的结构,但是它们有完全不同的功能时。
例如:
Dogs and cats communicate in totally different ways. A dog barks, but a cat meows. We may define an interface
Animaland create a classDogand classCat, like shown here.public interface Animal { public void communicate(); } public class Dog implements Animal { public void communicate() { System.out.println("bark, bark!"); } } public class Cat implements Animal { public void communicate() { System.out.println("meow, meow..."); } }
Java 支持多接口实现:例如,如果我们定义了另一个接口,MovingObject如图所示。
public interface MovingObject {
void movingNorth();
void movingSouth();
}
类Car可以从两个接口实现,Auto和MovingObject,如下所示。
public class Car implements Auto, MovingObject {
...
public void movingNorth() {
// car moves North
}
public void movingSouth() {
// car moves South
}
}
二十六、继承——代码重用
作为 OOP 原则之一,继承旨在集中许多不同对象的公共功能。因此,它减少了许多类中的重复代码。
继承引入了两类:“超类”和“子类”子类继承自超类。超类和“基类”是一回事子类不仅包含从超类继承的所有方法和字段,还包含子类定义的其他方法和字段。
例如,我们定义了一个名为Sedan的新类,它继承了我们之前创建的Car类。Car类实现了一个名为Auto的接口。在Sedan类中,我们定义了一个布尔字段isFourDoorHatchback和一个名为isFourWheelDrive()的方法。
关键字extends用于描述Sedan从类Car继承的类。
public class Sedan extends Car {
public Boolean isFourDoorHatchback;
public Boolean isFourWheelDrive(){
return true;
}
}
然后我们在一个Driver类中创建一个main方法来使用Sedan类。
public class Driver {
public static void main(String[] args) {
Sedan sedan = new Sedan();
sedan.start();
sedan.stop();
sedan.turn();
sedan.back();
sedan.park();
sedan.setMaker("Toyota");
sedan.getMaker();
sedan.isFourDoorHatchback = true;
sedan.isFourWheelDrive();
}
}
如您所见,Sedan对象(即sedan)拥有从其超类Car继承的所有方法和字段。除此之外,Sedan类有自己的方法和字段。用同样的main方法,我们添加更多的代码:
Car car = new Sedan();
car.start();
car.stop();
car.turn();
car.back();
car.park();
car.setMaker("Toyota");
car.getMaker();
这个例子告诉我们,我们可以从一个超类(即Car)创建一个对象,这个超类是从它的子类(即Sedan)实例化而来的。它的超类下的所有方法和字段都像预期的那样可用,但是它的子类下的方法和字段不可访问。
如果我们尝试去做:
Sedan sedan2 = new Car();
我们将得到错误消息:
"Type mismatch: cannot convert from Car to Sedan".
这清楚地告诉我们,不允许我们创建一个子类对象(即Sedan)从它的超类(即Car)实例化。
然而,在 Java 中,它不支持多重继承。相反,它使用一个接口来实现多重继承在其他编程语言中试图实现的相同目标。
问题
-
下面哪个是正确的语法来表示类
A是B的子类?-
public class A : super B { -
public class B extends A { -
public class A extends B { -
public A(super B) { -
public A implements B {
-
-
考虑以下类别:
public class Vehicle {...} public class Car extends Vehicle {...} public class SUV extends Car {...}以下哪些是合法陈述?
-
Car c = new Vehicle(); -
SUV s = new SUV(); -
SUV s = new Car(); -
Car c = new SUV(); -
Vehicle v = new Car(); -
Vehicle v = new SUV();
-
二十七、封装和多态
除了“抽象”和“继承”,OOP 中还有另外两个原则,“封装”和“多态”
包装
您可能听说过“信息隐藏”这个短语,意思是将一个对象的详细实现隐藏在更高的抽象层次之后。信息隐藏主要是出于安全考虑,而封装是出于复杂性考虑,将数据和类实现细节保留在类内。然而,封装将内部数据和方法结合起来,使其内部数据可以通过其公共方法从外部访问。并且该类具有私有实例变量,这些变量只能由同一类中的方法访问。这有助于管理频繁更新的代码。这被称为:“封装变化的东西”,这是最佳实践设计原则之一。
在前面章节创建的Student类中,我们有以下私有字段和公共方法。
-
private int age;只能从类内部访问
-
public void setAge(int age);从外部可访问的 setter
-
public int getAge();从外部获取
这是一个简单的封装示例,说明了我们如何设置学生的年龄值以及如何访问年龄信息。
public class TestStudent {
public static void main(String[] args) {
Student student = new Student("John", "Doe");
/*
student.age = 20;
This line will give compiler error
age field can't be used directly as it is private
*/
student.setAge(20);
System.out.println("Student name: " + student.getFirstName() + " " + student.getLastName() + "; age: " + student.getAge());
}
}
抽象和封装是有区别的。抽象是通过使用接口隐藏复杂性(即实现细节),而封装是将代码(即实现)和数据(即变量值)包装在同一个类中。
多态性
“聚”的意思是很多。“Morph”表示形式或形状。“多态性”是一个对象用许多不同的形式呈现同一界面的能力。在 Java 编程设计中有很多这样的例子。
-
通过一个接口,我们可以创建多个类。每个类实现相同的方法,但细节不同。
-
在基本类设计中,我们可以用不同的输入参数创建多个构造函数。
-
类似地,我们可以在类设计中使用相同的方法名和不同的输入参数集。这也称为“重载方法”
-
在子类中,我们可以“覆盖一个方法”,这个方法最初是在它的超类中定义的。
问题
-
写一个名为
GeometricObject的接口,声明两个抽象方法:getPerimeter()和getArea(). -
用受保护的变量
radius编写实现类Circle,实现接口GeometricObject。
二十八、数组——一种简单高效的数据结构
当我们需要存储和操作一堆相同类型的数据时,我们需要考虑使用正确的数据结构。假设我们想要处理表示相同类别的数据,例如您学校的学生姓名和年龄。数据将需要被分类、查询或搜索,并且容易访问。而且,我们有时可能需要更新或删除一些数据。
Java 提供了一种称为数组的简单数据结构来满足这些需求。数组提供大量存储空间来容纳我们的数据。存储空间的每个元素的标签称为“索引”它是一个从 0 开始的整数。存储在数组中的数据可以是整数、字符或其他类型的数据。
例如:
定义了一个总共有 7 个元素的整数数组
numbers
int[] numbers = new int[7]
定义了一个总共有 4 个元素的字符数组
letters
char[] letters = new char[4]
有不同的方法来分配或更新数组中的元素值。
- 如果您必须为每个元素分配不同的值,您需要声明数组及其大小,然后为每个元素分配值,如下所示:
int[] numbers = new int[5];
numbers[0] = 1;
numbers[1] = 3;
numbers[2] = 2;
numbers[3] = 4;
numbers[4] = 5;
或者:
- 如果数组元素中的值有明确的模式,可以按以下方式赋值:
int[] numbers = new int[ ] { 1, 3, 2, 4, 5 };
int[] numbers = new int[7];
for (int i = 0; i < numbers.length; i++) {
numbers[i] = 2 * i + 1;
}
数组的属性numbers.length存储数组numbers的大小值。
我们可以在运行时根据输入定义数组的大小,如下所示:
int k = scan.nextInt();
int[] numbers = new int[k];
例子
下面哪个选项是声明和初始化一个 8 整数数组的正确语法?
-
int a[8]; -
[]int a = [8]int; -
int[8] a = new int[8]; -
int[] a = new int[8]; -
int a[8] = new int[8];
回答
(d)
实验室工作
-
写一行代码声明并初始化一个名为
data的整数数组变量,元素值为 7,-1,13,24,6。 -
编写代码,创建一个名为
odds的数组,使用一个for循环将-16 到 48 之间的所有奇数存储到该数组中。确保数组有恰好合适的容量来存储这些奇数。
问题
-
下面哪一个选项是用一列特定值初始化五个整数的数组的正确语法?
-
int a { 14, 88, 27, -3, 2019 }; -
int[] a = new { 14, 88, 27, -3, 2019 } [5]; -
int[5] a = { 14, 88, 27, -3, 2019 }; -
int[] a = { 14, 88, 27, -3, 2019 }; -
int[] a = new int[ ] { 14, 88, 27, -3, 2019 };
-
-
执行以下代码后,数组编号有哪些元素值?
int[] numbers = new int[8]; numbers[1] = 4; numbers[4] = 99; numbers[7] = 2; int x = numbers[1]; numbers[x] = 44; numbers[numbers[1]] = 11;
二十九、常见陷阱
在这一章中,我想分享几段揭示编码实践中常见问题的代码。使用这些例子来诊断根本原因将有助于提高您的理解。我建议先独立思考,再寻求答案。你可能会在最后一章找到一些提示。
实验室工作
-
这里有什么问题吗?
String aAsString; String bAsString; Scanner user_input = new Scanner(System.in); System.out.println("a="); aAsString = user_input.next(); a = Integer.valueOf(aAsString); System.out.println("b="); bAsString = user_input.next(); b = Integer.valueOf(bAsString); -
这里有错误吗?
public class TestArray { public static void main(String[] args) { int[] myArray = new int[] { 11, 12, 13, 14, 15 }; System.out.printf("%d\n", myArray[5]); } } -
理解下面这个函数试图做什么,并思考如何改进它。
public static int CountStrings(String[] stringsArray, String countMe) { int occurences = 0; if (stringsArray.length == 0) { return occurences; // or, return 0; } for (int i = 0; i < stringsArray.length; i ++) { if (stringsArray[i].toLowerCase().contains(countMe.toLowerCase())) { occurences ++; } } return occurences; } -
发现缺陷:
public class Rectangle { public int width; public int height; public int getArea() { return width*height; } } public class SomethingIsWrong { public static void main(String[] args) { Rectangle myRect; myRect.width = 40; myRect.height = 50; System.out.println("myRect's area is " + myRect.area()); } } -
发现缺陷:
Scanner newscanner = new Scanner(System.in); System.out.print("Please enter today's date (month day):"); int z = newscanner.nextInt(); int y = news scanner.netInt(); if (z > 12 || y > 31) { System.out.println("You have entered an invalid number."); return; } else if (y > 31 && z > 12) { System.out.println("Both numbers you have entered are invalid."); return; } -
发现缺陷:
System.out.println("What month were you born in? (1-12)"); Scanner sc = new Scanner(System.in); String a = sc.nextLine(); Integer result = Integer.valueOf(a); int al = result.intValue(); -
发现缺陷:
if (numToTake >= 2 && numToTake< 3) { numToTake = 2; } else if (numToTake > 2) { System.out.println("The number you have entered is invalid."); }
三十、设计考虑
我们已经学习了一些关于 Java 中的类和对象的基本概念。现在让我们从类设计的角度来看几个例子。
实际案例 1
下面是一个Rectangle类的设计。它希望计算矩形的面积、周长和对角线,给定其宽度和高度值作为输入参数。
public class Rectangle {
private int width;
private int height;
private int area;
private double diagonal;
private int perimeter;
public Rectangle (int width, int height) {
this.width = width;
this.height = height;
this.area = width*height;
this.diagonal = Math.sqrt(width * width + height * height);
this.perimeter = (width + height) * 2;
}
public int getArea() {
return this.area;
}
public double getDiagonal() {
return this.diagonal;
}
public int getPerimeter() {
return this.perimeter;
}
}
面积、参数和对角线的计算在Rectangle构造函数中完成,每次初始化Rectangle类的对象时都会执行。如果我们一直想得到矩形的面积、周长和对角线的值,这是可行的。但是,当我们有时只想查询矩形的面积、周长或对角线时,某些部分的计算会变得过多。更好的设计方法是“按需”实现,如下所示。
public class Rectangle {
private int width;
private int height;
public Rectangle (int width, int height) {
this.width = width;
this.height = height;
}
public int getArea() {
return this.width * this.height;
}
public double getDiagonal() {
return Math.sqrt(this.width * this.width
+ this.height * this.height);
}
public int getPerimeter() {
return (this.width + this.height) * 2;
}
}
实际案例 2
下面的例子是一个Game类设计的实现。除了几个私有字段类型设计选择之外,它看起来不错。
-
商品的价格通常是一个小整数加上小数点右边的两位小数。由于浮点运算的不准确性,无论是
float类型还是double类型都无法准确表示这种用于货币计算的数字形式。建议用美分来表示美元价格,因此您只需要程序来处理整数计算。在某些情况下,用美元计算货币可能就足够了。 -
gameType不应该被定义为一个true/false布尔值。它应该使用“String数据类型。(或者,如果我们知道gameType的固定名称列表,我们可以考虑使用枚举法。)public class Game { private int price; private boolean gameType; private String platform; public Game() { } public int getPrice() { return this.price; } public int setPrice(int price) { return this.price=price; } public boolean getGameType() { return this.gameType; } public boolean setGameType(boolean gameType) { return this.gameType=gameType; } public String getPlatform() { return this.platform; } public String setPlatform(String platform) { return this.platform=platform; } }
实际案例 3
我们如何测试我们在 Eclipse 中设计的一个类?
至少有两种简单的方法。假设你已经设计了一个名为MyClass的类。它有一个公共整数数据字段- myNumber,和一个使其整数值加倍的方法- doubleMe()。
方法
原始类和测试代码都包含在一个 Java 文件中,如下所示:
public class MyClass {
// class design part of code
public int myNumber;
public MyClass() { }
public int doubleMe() {
return this.myNumber * 2;
}
// test part of code
public static void main(String arg[]) {
// declare and initialize an object
MyClass myObject = new MyClass();
myObject.myNumber = 2019;
int output = myObject.doubleMe();
// output the resulting data and validate it
System.out.println("My result is: " + output);
}
}
方法
以下两个类位于不同的 Java 文件中:
在MyClass.java中:
public class MyClass {
public int myNumber;
public MyClass() {
}
public int doubleMe() {
return this.myNumber * 2;
}
}
在TestMyClass.java中:
public class TestMyClass {
public static void main(String arg[]) {
MyClass myObject = new MyClass();
myObject.myNumber = 2019;
int output = myObject.doubleMe();
System.out.println("My result is: " + output);
}
}
实际案例 4
静态和非静态字段或方法之间有什么区别?我们什么时候使用静态字段和静态方法?
在前面描述的大多数代码示例中,我们使用了非静态字段和方法(也称为实例字段和实例方法)。实例字段和实例方法都属于被实例化的对象,这意味着它们直到对象被创建后才被激活。
但是,静态字段和静态方法属于类级别。它们可以由类名访问,而不是由从该类实例化的任何对象访问。存储在静态字段中并由静态方法计算的值在从同一个类创建的所有对象之间共享。
第一个也是我们最熟悉的静态方法是"main()"方法,如果您还记得的话。它可以驻留在任何公共类中。这个方法是任何应用程序的唯一入口点。它必须与一个类相关联。换句话说,它不存在于任何对象实例中。
在Demo类示例中,有一个静态字段counter,用于跟踪运行时创建的对象数量。有一个非静态字段(即实例字段)- myNumber与一个单独的对象实例相关联。非静态方法(即实例方法)- getNumber()也属于被创建的对象。
public class Demo {
private static int counter;
public static int getCounter() {
return counter;
}
private int myNumber;
public int getNumber() {
return this.myNumber;
}
public Demo(int number) {
this.myNumber = number;
counter++;
System.out.println("I am no. " + counter + " object so far.");
}
}
接下来是一个测试类,演示静态字段(即counter)和静态方法(即Demo.getCounter())是如何工作的,并与非静态字段(即myNumber)和非静态方法(即getNumber())进行比较。
public class TestDemo {
public static void main(String[] args) {
Demo demo1 = new Demo(21);
System.out.println("demo1 myNumber: " + demo1.getNumber());
System.out.println("object counts: " + Demo.getCounter());
Demo demo2 = new Demo(57);
System.out.println("demo2 myNumber: " + demo2.getNumber());
System.out.println("object counts: " + Demo.getCounter());
Demo demo3 = new Demo(99);
System.out.println("demo3 myNumber: " +
demo3.getNumber());
System.out.println("object counts: " + Demo.getCounter());
}
}
控制台的输出是:
I am no. 1 object so far.
demo1's myNumber: 21
object counts: 1
I am no. 2 object so far.
demo2's myNumber: 57
object counts: 2
I am no. 3 object so far.
demo3's myNumber: 99
object counts: 3
三十一、IOU 计算
IOU 的意思是“交集大于并集”它被用作图像检测技术中的度量。此指标计算两个矩形之间的重叠面积与其联合面积的比率。为了简单起见,这两个矩形在同一个方向,正如你在图 31-1 中看到的 R1 和 R2。
图 31-1
两个矩形及其重叠
为了计算这个比率,我们需要找出它们的重叠区域 x。如果两个矩形的面积分别是 R1.area 和 R2.area,那么
- IOU = X/(R1 . area+R2 . area–X)
我们用x_min、y_min、x_max和y_max来定义矩形的位置。它的四个顶点可以用四个坐标来表示:(x_min、y_min)、(x_min、y_max)、(x_max、y_max)、(x_max、y_min),从左下顶点开始,顺时针方向。
我们先来看看什么情况下 R1 和 R2 不会有重叠区域,如图 31-2 所示。
图 31-2
两个彼此分开的矩形
它将在以下时间出现:
-
R1.x_max <= R2.x_min,(1)
-
或者 R1.x_min >= R2.x_max,(2)
-
或者 R1.y_max <= R2.y_min,(3)
-
或者 R1.y_min >= R2.y_max (4)
如果从(1)到(4)的条件之一有效,则重叠面积为 0。
接下来,我们注意到重叠区域实际上被四条线所包围,如图 31-3 所示。
图 31-3
两个矩形及其重叠区域
-
x =最大值(R1.x_min,R2.x_min),x =最小值(R1.x_max,R2.x_max)
-
y =最大值(R1.y_min,R2.y_min),y =最小值(R1.y_max,R2.y_max)
根据数学推理,我们可以得出如下所示的编码设计方案:
有两类,Rectangle和IntersectionOverUnion。
Rectangle类为 x-y 坐标系上的矩形定义了一个数据模型。
public class Rectangle {
public float x_min;
public float x_max;
public float y_min;
public float y_max;
public Rectangle(float xmin, float ymin, float xmax, float ymax) {
if (xmin >= xmax || ymin >= ymax) {
throw new IllegalArgumentException("Not a valid rectangle!");
}
this.x_min = xmin;
this.y_min = ymin;
this.x_max = xmax;
this.y_max = ymax;
}
public float getWidth() {
return this.x_max - this.x_min;
}
public float getHeight() {
return this.y_max - this.y_min;
}
}
IntersectionOverUnion类包含驱动执行的main()方法。
public class IntersectionOverUnion {
public static void main(String[] args) {
// test case 1
Rectangle r1 = new Rectangle(3f, 2f, 5f, 7f);
Rectangle r2 = new Rectangle(4f, 1f, 6f, 8f);
System.out.println("IOU=" + getIOU(r1, r2));
// test case 2
r1 = new Rectangle(3f, 2f, 5f, 7f);
r2 = new Rectangle(1f, 1f, 6f, 8f);
System.out.println("IOU=" + getIOU(r1, r2));
// test case 3
r1 = new Rectangle(3f, 2f, 5f, 7f);
r2 = new Rectangle(6f, 1f, 7f, 8f);
System.out.println("IOU=" + getIOU(r1, r2));
}
public static float getIOU(Rectangle r1, Rectangle r2) {
float areaR1 = r1.getHeight() * r1.getWidth();
float areaR2 = r2.getHeight() * r2.getWidth();
float overlapArea = 0f;
if (r1.x_min >= r2.x_max || r1.x_max <= r2.x_min ||
r1.y_min >= r2.y_max || r1.y_max <= r2.y_min) {
return 0f;
}
overlapArea = computeOverlap(
Math.max(r1.x_min, r2.x_min),
Math.min(r1.x_max, r2.x_max),
Math.max(r1.y_min, r2.y_min),
Math.min(r1.y_max, r2.y_max));
System.out.println(overlapArea + " / (" + areaR1
+ " + " + areaR2 + " - " + overlapArea + ")");
return overlapArea / (areaR1 + areaR2 - overlapArea);
}
private static float computeOverlap(
float x1,
float x2,
float y1,
float y2) {
float w = x2 - x1;
if (w < 0) w = -w;
float h = y2 - y1;
if (h < 0) h = -h;
return w * h;
}
}
我们还没完。我们需要经常思考如何改进我们的类设计和优化代码。在Rectangle类中,有getWidth()和getHeight()方法。如果我们给Rectangle类添加一个叫做getArea()的方法会怎么样?
Rectangle类更新为:
public class Rectangle {
public float x_min;
public float x_max;
public float y_min;
public float y_max;
public Rectangle(float xmin, float ymin, float xmax, float ymax) {
if (xmin >= xmax || ymin >= ymax) {
throw new IllegalArgumentException("Not a valid rectangle!");
}
this.x_min = xmin;
this.y_min = ymin;
this.x_max = xmax;
this.y_max = ymax;
}
public float getWidth() {
return this.x_max - this.x_min;
}
public float getHeight() {
return this.y_max - this.y_min;
}
public float getArea() {
return this.getWidth() * this.getHeight();
}
}
剩下的代码看起来会像这样:
import java.lang.Math;
public class IntersectionOverUnion {
public static void main(String[] args) {
// test case 1
Rectangle r1 = new Rectangle(3f, 2f, 5f, 7f);
Rectangle r2 = new Rectangle(4f, 1f, 6f, 8f);
System.out.println("IOU=" + getIOU(r1, r2));
// test case 2
r1 = new Rectangle(3f, 2f, 5f, 7f);
r2 = new Rectangle(1f, 1f, 6f, 8f);
System.out.println("IOU=" + getIOU(r1, r2));
// test case 3
r1 = new Rectangle(3f, 2f, 5f, 7f);
r2 = new Rectangle(6f, 1f, 7f, 8f);
System.out.println("IOU=" + getIOU(r1, r2));
}
public static float getIOU(Rectangle r1, Rectangle r2) {
float areaR1 = r1.getArea();
float areaR2 = r2.getArea();
float overlapArea = 0f;
if (r1.x_min >= r2.x_max || r1.x_max <= r2.x_min ||
r1.y_min >= r2.y_max || r1.y_max <= r2.y_min) {
return 0f;
}
overlapArea = computeOverlap(
Math.max(r1.x_min, r2.x_min),
Math.min(r1.x_max, r2.x_max),
Math.max(r1.y_min, r2.y_min),
Math.min(r1.y_max, r2.y_max));
System.out.println(overlapArea + " / (" + areaR1
+ " + " + areaR2 + " - " + overlapArea + ")");
return overlapArea / (areaR1 + areaR2 - overlapArea);
}
private static float computeOverlap(
float x1,
float x2,
float y1,
float y2) {
float w = x2 - x1;
if (w < 0) w = -w;
float h = y2 - y1;
if (h < 0) h = -h;
return w * h;
}
}
面积的计算现在封装在Rectangle类中。这个变化本身并不大,但是我们应该习惯于在我们仍然能够逐步改进我们的程序设计的时候做一些小的改变。
三十二、项目
我想给你推荐一个动手项目清单,让你独立练习。完成这些项目肯定会帮助您加深对本书中描述的基本 Java 编程概念的理解。
项目甲
第一步
编写一个名为Rectangle的类,表示一个矩形的二维区域。构造函数创建一个新的矩形,其左上角由给定的坐标指定,并具有给定的宽度和高度。
public Rectangle(int x, int y, int width, int height)
您的Rectangle对象应该有以下方法:
-
public int getHeight()-返回这个矩形的高度。 -
public int getWidth()-返回这个矩形的宽度。 -
public int getX()-返回这个矩形的 x 坐标。 -
public int getY()-返回这个矩形的 y 坐标。 -
public String toString()-返回该矩形的字符串表示,例如:
"Rectangle[x=1,y=2,width=3,height=4]"
第二步
将前面练习中的以下存取方法添加到您的Rectangle类中:
public boolean contains(int x, int y)
public boolean contains(Point p)
Point类的定义如下所示:
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
两个contains()方法返回给定的Point或坐标是否在这个Rectangle的边界内的布尔状态。例如,一个[x=2,y=5,width=8,height=10]的矩形对于(2,5)到(10,15)之间的任何一点都将返回 true,这意味着包括了边。
项目
给定四个输入值,设计一个程序来找出当前日期和用户生日之间的天数。
程序提示输入用户的生日。提示会列出可供选择的值范围。请注意,打印的天数范围基于用户键入的月份中的天数。该程序打印生日的绝对日期。1 月 1 日是绝对日#1,12 月 31 日是绝对日#365。最后,程序打印离用户下一个生日还有多少天。如果生日是今天或明天,会出现不同的信息。下面是程序的四次运行及其预期输出(用户输入数据就在“?”后面)马克):
Please enter your birthday:
What is the month (1-12)? 11
What is the day (1-30)? 6
11/6 is day #310 of 365.
你的下一个生日在 105 天后,从今天算起。
项目 C
游戏规则是这样的:你从 21 根棍子开始,两个玩家轮流拿一根或两根棍子。拿最后一棒的玩家输了。你能设计一个程序来模拟游戏中两个玩家中的一个吗?一个玩家是用户,另一个玩家是电脑。
项目 D
编写一个名为hasVowel()的方法,返回一个字符串是否包含任何元音(一个包含 a、e、I、o 或 u 的单字母字符串,不区分大小写)。
项目 E
编写一个名为gcd()的方法,该方法接受两个整数作为参数,并返回这两个数字的最大公约数(GCD)。两个整数 a 和 b 的 GCD 是同时是 a 和 b 的因子的最大整数,任意数和 1 的 GCD 是 1,任意数和 0 的 GCD 是数。
计算两个数字的 GCD 的一个有效方法是使用欧几里德算法,该算法表述如下:
GCD(A,B) = GCD(B,A % B)
GCD(A,0)= A 的绝对值
例如:
-
gcd(24, 84)返回 12 -
gcd(105, 45)返回 15 -
gcd(0, 8)退货 8
项目
编写一个名为toBinary()的方法,该方法接受一个整数作为参数,并以二进制形式返回该数字的字符串表示。比如toBinary(42)的调用应该会返回“101010”。
项目
使用下列卡片上的四个数字创建一个等于 24 的数学表达式。每张卡只能使用一次。把 ace 当成数字“1”。您可以在数学表达式中使用+、-、*、/、(和)。请找出所有可能的答案。
三十三、Java 进阶解决方案
作为参考,在这一章中,我将为你提供前几章中一些问题的答案提示。比如“为了 16。”意思是“第十六章中问题的提示”
为了 16。毕达哥拉斯三元组
-
不使用“c”,我们可以检查(a 2 + b 2 )是否是一个完美的平方数,取它的平方根并验证它是否是一个整数值。
-
使用示例代码并检查结果值(a 2 + b 2 )是否与“4n + 1”的形式相匹配。
为了 17。强类型编程
public boolean isCollinear(Point p) {
if (p.getX() == p1.getX() && p1.getX() == p2.getX()) {
return true;
}
if (this.getSlope(p) == this.getSlope()) {
return true;
}
return false;
}
public double getSlope(Point p) {
if (this.p1.x == this.p.x) {
throw new
IllegalStateException("Denominator cannot be 0");
}
return (double)(this.p.y - this.p1.y) / (this.p.x - this.p1.x);
}
18 岁。条件语句
-
它被重写,如下所示。
if (num < 10 && num > 0) { System.out.println("It's a one-digit number"); } else if (num < 100) { System.out.println("It's a two-digit number"); } else if (num < 1000) { System.out.println("It's a three-digit number"); } else if (num < 10000) { System.out.println("It's a four-digit number"); } else { System.out.println("The number is not between 1 & 9999"); } -
这里显示了一个简化的版本。
if (a == 0) { if (b == 0) {...} else {...} } else { if (b != 0) {...} }
为了 19。switch语句
switch(color) {
case 'R':
System.out.println("The color is red");
break;
case 'G':
System.out.println("The color is green");
break;
case 'B':
System.out.println("The color is black");
break;
case 'C':
default:
System.out.println("Some other color");
break;
}
21 岁。计算
-
定义 x 为孩子的数量,(2200–x)为成年人的数量,那么 1.5∫x+4∫(2200–x)= 5050。迭代 x = 0 到 2200,找到 x 的一个解。很明显,不存在一个以上的解。
-
将 x 定义为正确答案的数量,将(10–x)定义为错误答案的数量,则 5∫x–2(10–x)= 29。从 0 到 10 迭代 x,找到 x 的可能解。
-
迭代一个从 0 到 2001 的正整数,并检查它与 3、4 和 5 的整除性。
-
迭代每个三位数的整数,从 100 到 999,并检查其位数。
-
用一个递归的方法(参考例子)从三类植物(定义三类为 A、B、C)中重复挑选一个植物五次。然后从组合中删除重复项。例如:{A,A,B,B,C}是{A,B,A,B,C}的重复。
为了 23。Pi 的探索性实验
利用以下带有整数“r”的公式,近似计算“e”的值
为了 24。面向对象编程中的类
-
a)
-
b)
-
NumberHolder nh = new NumberHolder(); Nh.anInt = 5; Nh.aFloat = 3.2; System.out.printIn("anInt=" + Nh.anInt + "; aFloat=" + Nh.aFloat); -
(一)、(四)
-
(二)
26 岁。继承——代码重用
-
(c)
-
(b)、(d)、(e)、(f)
为了 27。封装和多态
-
public interface GeometricObject { public abstract double getPerimeter(); public abstract double getArea(); } -
public class Circle implements GeometricObject { private final double PI = 3.14159; protected double radius; public Circle(double radius) { this.radius = radius; } // Implement methods defined in the interface @Override public double getPerimeter() { return 2 * PI * this.radius; } @Override public double getArea() { return PI * this.radius * this.radius; } }
28 年。数组——一种简单高效的数据结构
-
(d)
-
{ 0, 4, 0, 0, 11, 0, 0, 2 }
29 元。常见陷阱
-
如果想得到一个整数值,为什么不一开始就取一个整数输入呢?
This is a corrected version. It is significantly simplified.
Scanner user_input = new Scanner(System.in); System.out.println("a="); int a = user_input.nextInt(); System.out.println("b="); int b = user_input.nextInt(); -
myArray[3]等于“13”吗?注意数组元素索引的定义。
-
有必要查
stringsArray.length = 0吗?在for循环中做countMe.toLowerCase()是一个好方法吗?This is a recommended version:
public static int CountStrings(String[] stringsArray, String countMe) { int occurences = 0; String keyword = countMe.toLowerCase(); for (int i = 0; i < stringsArray.length; i ++) { if (stringsArray[i].toLowerCase().contains(keyword)) { occurences ++; } } return occurences; } -
myRect初始化过吗?There is an important line to update in the
main()method as shown here:public class SomethingIsWrong { public static void main(String[] args) { Rectangle myRect = new Rectangle(); myRect.width = 40; myRect.height = 50; System.out.println("myRect's area is " + myRect.area()); } } -
既然变量
temp已经被赋予了array1中第一个元素的值,我们需要从for循环中的i=0开始迭代吗?The simple fix is to change from
for (int i = 0; ... to for (int = 1; ...in the original function as shown.public static int getMaxLength(ArrayList<String> array1) { if(array1.isEmpty()) { return 0; } else { String temp= array1.get(0); for (int i = 1; i < array1.size(); i++) { if (array1.get(i).length() > temp.length() ) { temp= array1.get(i); } } return temp.length(); } } -
检查
if/else子句。“
y > 31 && z > 12的范围已经被“z > 12 || y > 31的范围覆盖。因此,原代码中的“else if (...)部分毫无意义。 -
查看
Scanner的实际使用情况。Due to the same reason stated in 1, the code can be corrected as shown:
System.out.println("What month were you born in? (1-12)"); Scanner sc = new Scanner(System.in); int al = sc.nextInt(); -
检查
if/else子句numToTake > 2的范围已经包含了numToTake >= 2 && numToTake < 3的范围。if和else if条件句需要重写。
第一部分:Java 基础
第二部分:Java 进阶
Java Intermediate
读者在阅读本部分之前应该已经完成了第一部分:Java Basic。第二部分重点介绍我们如何学习 Java 编程,并整合基本的数学概念。
同样在第二部分中,我们通过许多实际例子演示了如何将 Java 编程应用于数学问题的解决。
读者将有机会见证 Java 编程如何在我们的实验工作中成为一个强大的工具。
我相信你会很高兴在这部分找到许多有趣的应用例子。虽然这本书没有触及每一个细节,但它将涵盖类和面向对象编程的基本概念,以便初学者能够建立一个良好的基础。