Black Boxes
In section 1.5 we discussed the importance of writing good computer programs. Among the goals that we have for our programs as we write them are that we want them to be:
In this lesson we will be looking at methods. The use of methods is one of the most powerful methods we have for accomplishing the four goals listed above. We use methods to break a program down into smaller parts. You could think of each of these parts as a "module". As an analogy, let's think about electronic equipment. These days when you have your television or stereo repaired, no one actually goes in and messes around with individual wires. The equipment is made up of modules. If a module breaks, the repairperson removes it and replaces it with a new module. He doesn't need to know what is inside the module or how it works. He only needs to know what the module does.
(Actually, I have no idea whether this is really an accurate description of how electronics are repaired. It makes a great analogy, though.)
We would like our programs to work kind of like this. We want to break the program up into "modules," called methods. This way, once we write a method and debug it and test it, we can forget about how it works and what's inside, and just remember what it does. This is often called the "black box" analogy. Once we have a particular method working perfectly, we treat it as a black box; that is, we imagine that we can't look inside to see what the details are of how it works, we just know what it does. We will see later that our black boxes (i.e. methods) can have carefully controlled information going in and out of them, and what a black box (i.e. method) does is often specified according to what goes in and what comes out.
An important thing to notice about these black boxes is that they are completely self-contained and independent. If one black box has access to what's inside another one, we can no longer treat them independently. In order for the black box concept to work, we have to be able to change how a black box works on the inside and know that the rest of our program will still work.
Top-Down Programming
In order to use this new "black box" strategy, we are going to have to change the way we look at programming a bit. Until now when we have gone to write a program we have written down every little detailed step, step by step, until we reached the end of the program. What we want to do now is look at the problem and instead of asking ourselves what the first (detailed) step is, and then what the next (detailed) step is, and then what the next (detailed) step is, and so on, we want to ask ourselves "what are the 3 or 4 or 5 major steps involved in accomplishing this task?" Once we have them down, then we go back and fill in the details. This is called top-down programming or stepwise refinement.
Suppose we are writing a computer program to bake a cake. By our old methods we would have started our program off something like this:
1) open cupboard door
2) get out baking pan
3) open refrigerator door
4) remove 1 egg
5) get out a small bowl
6) open the cupboard door
7) measure out one cup of flour
.
.
.
.
192) open oven door
193) insert baking pan
194) close oven door
195) wait 45 minutes
.
.
.
Using stepwise refinement, we would approach the problem like this:
1) gather ingredients
2) mix ingredients
3) bake
Then we would have to go back and define more precisely what we mean by each of these three steps.
The Advantages of Using Methods:
1) Our programs become easier to read. A long complicated program written without methods can be a gigantic task to try to read and understand. All of the details are right there in front of you and it's difficult to get the big picture of what the program is all about. If we use methods effectively, it becomes trivial to understand each individual method. Instead of being a huge thing with hundreds of confusing statements, the main method can become a list of three or four or five easy to understand steps. If we want more details about one (or more) of the individual steps, we can go look at the method definitions.
2) Our programs become easier to debug. Imagine that we have a huge program all written out in one huge main method. Further assume that all the different parts of the program are constantly messing with numerous different variables. If there is a problem with our program, it will be very difficult to isolate the problem and fix it. Furthermore, even after we find the place in the program where things are going wrong, it is likely that in the process of fixing the problem we will accidentally introduce problems elsewhere in the program, because we have no guarantee that a change made in this part of the program won't affect something going on in another part of the program. What a nightmare! However, if we use methods, it is easy to go through the Methods one by one, making sure that they work correctly, until we find the one that is defective. Then we can forget about the rest of our program and focus on that one method. Again, it is important to keep our methods self-contained and independent, so we know that no other part of our program will be messed up because of the change we are making here.
3) Our programs become easier to modify. When we use methods and we want to make a modification to our program it is easy to locate the method that needs to be changed. As we make the modification, we don't have to worry about whether our changes will adversely affect some other part of our program, because our methods are independent.
4) Parts of our programs become easier to reuse. Now that we have split our program up into methods, if we find ourselves writing another program where we need to do a similar task, we can easily remove the method from one program and insert it into the new program. We don't have to concern ourselves with what is inside the method, only with what the method does. The extent to which this is true can be greatly influenced by how we write our methods and what names we choose for our methods. During the discussion that follows, we will be careful to write and name methods to maximize this benefit of using methods.
Well, it's time we actually saw a concrete example of using methods. Let's write a program that produces the following output:
XXXXXX
X X
X X
XXXXXX
Although this is a very specific picture we are drawing, let's write a solution that is "general"; that is, we would like to make it so that by simply changing one number in our program we could change the width or the height of this picture.
Additional note: for the sake of illustration, we will be writing our program without the setw method, even though that method could simplify our code.
Before studying methods we might have started by thinking about the details: how will we print each individual "X"? Instead, we begin by breaking the problem down into steps. Look at the box for a minute. How would you break the task down into 2 or 3 or 4 steps? I would suggest breaking it down into the sections marked by the horizontal lines in this picture:
XXXXXX
X X
X X
XXXXXX
What would you choose for the names of the methods to perform each of the three subtasks we have illustrated here? A common first suggestion goes something like this:
1) do top
2) do middle
3) do bottom
There are some problems with the names of these methods. First, they aren't very precise. What do we mean by "do"? Let's replace the word "do" with the word "draw".
Furthermore, if we choose these names for our methods we will be forced to write two different methods, one called "drawTop" and one called "drawBottom". But these two methods would do exactly the same thing! The next suggestion is often to simply call "drawTop" twice, like this:
1) drawTop
2) drawMiddle
3) drawTop
This still presents a problem. Now when we look at our main method it is confusing. If the task is to draw a box, why would we want to draw the top two times? The solution to this dilemma is to give our methods names that are not tied to the context of the program as a whole (drawTop only makes sense because the program is drawing a box!) but instead simply describe what the method does. In this case, the method in question draws a horizontal line. So we will call it "drawHorizontalLine".
What about step 2? Can we think of a name for that method that doesn't refer to a part of a box but instead simply describes what the method does? If you look carefully at the picture above, you will notice that the middle section of the box is really just two vertical lines. So we will call our second method "draw2VerticalLines". As you can see, it is important to think carefully about the names of your methods.
In conclusion, our steps will be:
1) drawHorizontalLine
2) draw2VerticalLines
3) drawHorizontalLine
Putting these into an actual program would look like this:
import java.util.Scanner;
public class Example {
public static void main(String[] args) {
drawHorizontalLine();
draw2VerticalLines();
drawHorizontalLine();
}
}
The three lines in this main method are called "method calls". Notice that they look just like statements in that they appear on a line all by themselves and have a semi-colon at the end. Also notice that each method call is followed by a set of parentheses. This will always be the case in Java. What we mean when we write a method call as a statement is "go execute all of the statements in the method called, and then return to this point in the program when you are done".
We are now done with our program except that we still have to define drawHorizontalLine() and draw2VerticalLines(). Here's the definition of drawHorizontalLine():
public static void drawHorizontalLine() {
for (int count = 0; count < 6; count++){
System.out.print("X");
}
System.out.println();
}
Notice that if we want to change the width of the box from 6 to some other number, the only change we would have to make in this method would be to change the 6. If we were using good programming style this 6 would be a named constant, but we are going to leave it as a 6 for now. Later on in this lesson that will change.
The definition of draw2VerticalLines() is a bit more involved. Once again, we need to postpone thinking about the details right away. We start by realizing that the task involves printing out 2 rows. This leads us to a partial solution:
public static void draw2VerticalLines() {
for (int rowCount = 0; rowCount < 2; rowCount++){
<insert code to print one row>
}
}
Each row will be a pair of stars with some spaces (4 in this case, since the width is 6), like this:
* *
Now we have a couple of options. One option would be to write the code to print one row and insert it right into this method. A better choice would be to make a new method called "drawOneRow." In general, anytime you can name a task which a sequence of statements is performing it is a good idea to make the sequence of statements into a method. Notice that if we had chosen to insert the code right here, we would have nested loops. Using a method call here is nice because nested loops tend to be difficult to read.
Here is the code for drawOneRow:
public static void drawOneRow()
{
System.out.print("X");
for (int spaceCount = 0; spaceCount < 4; spaceCount++){
System.out.print(" ");
}
System.out.println("X");
}
Below you will find the code for our complete "draw box" program. There are several things to notice. First, there are two distinct occurrences of each method name. For example
draw2VerticalLines()
occurs two times:
It occurs once in the main method. In this case, we place it there as an instruction. We are telling Java to go execute the method. It is called a method call. Notice that the word void does not appear, and the line ends with a semi-colon.
It occurs once where we define it. Not surprisingly, it is called a method definition in this case. The first line of a method definition is called the method header.
It is important when writing methods to leave lots of whitespace between method definitions. It makes it much easier for someone reading your program to identify individual methods and to discern where one method ends and the next one begins. In programs that you are turning in, you should put at least 6 blank lines between method definitions. In order to save space, future examples in these notes may not follow this guideline; however, you should.
public class Example {
public static void main(String[] args) {
drawHorizontalLine(); // these three lines are
draw2VerticalLines(); // "method calls"
drawHorizontalLine();
}
public static void drawHorizontalLine() { // this is a "method definition"
for (int count = 0; count< 6; count++){
System.out.print("X");
}
System.out.println();
}
public static void draw2VerticalLines() { // this is another "method definition"
for (int rowCount = 0; rowCount < 2; rowCount++){
drawOneRow();
}
}
public static void drawOneRow() {
System.out.print("X");
for (int spaceCount = 0; spaceCount < 4; spaceCount++){
System.out.print(" ");
}
System.out.println("X");
}
}
Let's take another look at the program that we wrote in section 6.3. Instead of always drawing a rectangle with a width of 6 and a height of 4, let's modify the program so that the user enters the height and width of the rectangle. Here's what our new main method might look like:
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int width;
int height;
System.out.print("Enter width: ");
width = input.nextInt();
System.out.print("Enter height: ");
height = input.nextInt();
drawHorizontalLine();
draw2VerticalLines();
drawHorizontalLine();
}
Taking a look at our drawHorizontalLine method, it looks like we only need to change the 6 to width, like this:
public static void drawHorizontalLine() {
for (int count = 0; count < width; count++){
System.out.print("X");
}
System.out.println();
}
This is not a bad first try. The problem is, we have violated our most important rule about methods: that they should be self-contained and independent. The way we have written this, we are assuming that the value of width used in drawHorizontalLine will get its value by looking back at the value of width in the main method. But what if we take drawHorizontalLine out of this program and try using it in another program? drawHorizontalLine is no longer self-contained: it requires that width be carefully set to an appropriate value before drawHorizontalLine is called. As another example, what if we were dealing with a large, involved program and some other method somewhere inadvertently changed the value of width before drawHorizontalLine got called? The way we have written this program, even if we did get the program to work correctly, it would be difficult to modify because every time we change a variable we have to check with all the other methods in our program to make sure that it is ok that we change the variable, and that we won't interfere with something that some other method is doing.
As it turns out, the strategy we used for the program above won't even work in Java. We would get a syntax error. But we have a problem. Somehow drawHorizontalLine() needs to find out what the width of the box should be. In order to deal with this kind of situation, where several methods need to have access to the same information, Java has set up a very careful system of communicating between methods.
When main() calls drawHorizontalLine(), it needs to tell drawHorizontalLine() how many "X"s to print out. In order to do this, we put the value that main() is communicating to drawHorizontalLine(), width in this case, in parentheses after the method call:
drawHorizontalLine(width);
The way to interpret the method call is that when main() calls drawHorizontalLine() it is telling it to do its job and at the same time is sending a value -- whatever the value is that happens to be stored in the variable width. When we put a variable in parentheses after a method call like this the variable is called an argument.
Let's look at the situation from the perspective of drawHorizontalLine() now. drawHorizontalLine() needs to inform Java that when it gets called it must be sent a value. We indicate this by putting a variable declaration in the parentheses after the words drawHorizontalLine in the method definition header:
void drawHorizontalLine(int numXs)
This variable declaration is called a parameter. The mechanism works like this: When drawHorizontalLine() gets called, the method that called it must send a value. When drawHorizontalLine() begins execution, the variable that is declared in the parentheses is initialized to this value. For example, if the method that calls drawHorizontalLine() sends the value 7, then the variable numXs gets initialized to 7. The words int numXs in this example are really a variable declaration, just the same as if we had declared the variable on the first line of the method itself. The only difference is that local variables normally start with no initial value, but because numXs is a parameter, it will begin with an initial value.
Here is the correct version of main() and drawHorizontalLine():
public static void main(String[] args)
{
int width;
int height;
System.out.print("Enter width: ");
width = input.nextInt();
System.out.print("Enter height: ");
height = input.nextInt();
drawHorizontalLine(width);
draw2VerticalLines();
drawHorizontalLine(width);
}
void drawHorizontalLine(int numXs)
{
for (int count = 0; count < numXs; count++){
System.out.print("X");
}
System.out.println();
}
Even though when we look at this whole program at once, we see that the variable width in main and the variable numXs in drawHorizontalLine look like they are the same thing, we have carefully chosen to give them different names. This is to illustrate the fact that they are not the same variable! This example leads us to the following two important rules of how to view methods and parameters:
1) When writing a method always look at the situation from the perspective of the method you are writing.
One way of doing this is to pretend that you are that method. For example, when we are writing the method drawHorizontalLine(), we say to ourselves, "if I am method drawHorizontalLine(), what do I need to know to do my job? Well, I need to know how many Xs to print out!" Notice that when we are looking at the situation from the perspective of the method drawHorizontalLine(), we don't even realize that we are drawing a box! That is why it wouldn't make any sense to call the parameter width instead of numXs. Our job doesn't have anything to do with boxes, it only has to do with drawing a horizontal line. An advantage of this approach, besides the simple fact that it keeps our method independent of the rest of the program, is that now if we want to use the drawHorizontalLine() method in another program that doesn't have anything to do with boxes, it still makes sense: in order to do our job, we need to know how many Xs to print out. If you are tempted to call the parameter width instead of numXs, it probably means that you are still thinking about the whole program as one entity instead of thinking of methods as distinct entities that should be self-contained and not dependent on the rest of the program.
Now let's switch and look at the situation from the perspective of main(). Main() says, "ok, I want drawHorizontalLine() to do its job, but it needs me to send it one value -- the value that tells it how many Xs to print out. I'm storing the number of Xs that I want printed out in my variable width, so that is the value I will send." So, in main(), width goes in the parentheses because that is the correct value to send.
2) Think of parameter passing as communication, not as a sharing of variables.
If you start thinking of width and numXs as being different names for the same variable you will get in big trouble. They are not the same variable. NumXs is simply the place where drawHorizontalLine() stores whatever value was sent when it was called.
It is extremely important to realize that what is being passed from one method to another here is not a variable. It is simply a value. In fact, it would be perfectly legal to put a value or even an expression in the parentheses. For example we could have a program where we called the method drawHorizontalLine() like this:
drawHorizontalLine(3);
which would mean to print 3 Xs. Or we could say
drawHorizontalLine(2*width-1);
so that, for example if width was 7, drawHorizontalLine would print out 13 Xs.
Because what is being passed is a value and not a variable, this parameter passing mechanism is called pass-by-value.
Let's get back to our drawBox program. We haven't finished it yet: we have only fixed drawHorizontalLine(). We still have to fix draw2VerticalLines(). We begin by following rule number 1 from above and looking at the situation from the perspective of the draw2VerticalLines() method. We ask ourselves, "ok, if I'm method draw2VerticalLines(), and my whole purpose in life is to draw two parallel vertical lines of stars when I am called, what information do I need in order to do my job? Well, I need to know how many spaces to put between the two lines, and I need to know how many horizontal rows to print." A horizontal row is this:
* *
So, for example, 4 rows would look like this:
* *
* *
* *
* *
Now we choose names for the two pieces of information that we need. We'll call the number of spaces numSpaces and the number of horizontal rows numRows. These two variables will go in the parentheses in our method header. They will be our parameters. After going through a similar process to decide what the parameters for drawOneRow should look like, the result will be this:
public static void draw2VerticalLines(int numSpaces, int numRows) {
for (int rowCount = 0; rowCount < numRows; rowCount++){
drawOneRow(numSpaces);
}
}
public static void drawOneRow(int numSpaces) {
System.out.print("X");
for (int spaceCount = 0; spaceCount < numSpaces; spaceCount++){
System.out.print(" ");
}
System.out.println("X");
}
To complete our task now we have only to make the necessary changes inside main() to match the changes we have made in draw2VerticalLines(). To do this we must once again switch to looking at the situation from main()'s perspective. From main()'s perspective, draw2VerticalLines() is asking for two pieces of information: the number of spaces and the number of rows. How many spaces does main() want draw2VerticalLines() to print between the lines?? The answer is "2 less than the width of the box," or width - 2. How many rows does main() want draw2VerticalLines() to print? The answer is similar: "2 less than the height of the box," or height - 2. So these two expressions will be placed in the parentheses after the call to method draw2VerticalLines().
Here is the complete version of our new program which allows the user to input the height and width of the box to be drawn.
import java.util.Scanner;
public class Example {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int width;
int height;
System.out.print("Enter width: ");
width = input.nextInt();
System.out.print("Enter height: ");
height = input.nextInt();
drawHorizontalLine(width);
draw2VerticalLines(width - 2, height - 2);
drawHorizontalLine(width);
}
public static void drawHorizontalLine(int numXs) {
for (int count = 0; count < numXs; count++){
System.out.print("X");
}
System.out.println();
}
public static void draw2VerticalLines(int numSpaces, int numRows) {
for (int rowCount = 0; rowCount < numRows; rowCount++){
drawOneRow(numSpaces);
}
}
public static void drawOneRow(int numSpaces) {
System.out.print("X");
for (int spaceCount = 0; spaceCount < numSpaces; spaceCount++){
System.out.print(" ");
}
System.out.println("X");
}
}
Declaring Variables Locally
Every variable must be declared inside the method in which it is used.
The word local means "inside the method in which it is used." So, a more concise way of saying this rule is
Every variable must be declared locally.
Note that a method's parameters count as local variables.
It's possible to declare variables outside of any method but inside the class. These are called class variables, and they are accessible from any method. The problem is that if we have global variables, our methods are no longer independent and self-contained. Until I tell you otherwise, do not use any class variables.
So far in this lesson when we have called a method we have used the method call as if it were a statement -- in other words, we placed the method call on a line all by itself, followed by a semi-colon, like this:
drawHorizontalLine(width);
This makes sense because, as with other statements, when we call drawHorizontalLine() we are giving the computer an instruction, telling it to do something. It is also possible to write methods that should be called as if they are expressions (instead of statements). For example:
x = squareRootOf(25);
This makes sense because in this case we are not giving the computer an instruction, we are asking the computer to determine a value, which is what expressions do.
The first type of method, the type used as a statement, is called a void method. The second type of method, the type used as an expression, is called a value-returning method.
Before moving on, let me point out that most students learning about methods do not have any trouble learning how to define value-returning methods. The hard part is usually understanding how to call them. So let me belabor the point just a bit more by using my imaginary "squareRootOf" method as an example. If I want to print out the square root of 25, I would write it like this:
System.out.println(squareRootOf(25));
The mechanism here is the same as if I had written
System.out.println(7 * 5);
You can think of it like this. In both cases, Java goes off and evaluates the expression. (In the first case the expression is squareRootOf(25). In the second case the expression is 7 * 5). When the expression is evaluated, Java replaces the expression with the result. So after the expression has been evaluated in the first example, Java sees the println() statement as
System.out.println(5);
(since the square root of 25 is 5). After the expression in the second example has been evaluated, Java sees the println() statement as
System.out.println(35);
A mistake that many students make is to think that before you can use a value-returning method as an expression you have to call it first as a void method to "initialize" it. So I often see this in code:
squareRootOf(25); System.out.println(squareRootOf(25));
The insidious thing about this error is that your program will still work. Java essentially ignores the first call to squareRootOf. However, code like this demonstrates a lack of understanding about how value-returning methods work, and will be penalized.
Now I'm ready to tell you about defining a value-returning method. The process is almost exactly the same as defining a void method. There are just two differences. First, you have to indicate the data-type of the value that is going to be returned. Second, you have to actually return the value.
As an example, let's design our own pow method. We'll limit our discussion to int's. Since our method will be returning an int, we will write the word int instead of void in front of the method name. So our method header will look like this:
public static int pow(int base, int power)
It would be an interesting exercise to develop the algorithm for calculating the result of this method; however, in the interest of time and space, I will simply provide the code for you. You should study it to make sure you understand why it works. The only other new thing in the method that follows is the statement
return result;
This is the second difference between defining a void method and defining a value-returning method: a value-returning method must have a return statement that tells Java what value the method will return. This is the value that will be substituted in the place of the method call in the calling method. Here is the final version of our pow method.
public static int pow(int base, int power)
{
int result = 1;
for (int count = 0; count < power; count++){
result *= base;
}
return result;
}
There is one operator in this method which may be new to you. The *= operator is simply a shorthand notation for "multiply and assign". So the statement
result *= base;
is equivalent to the statement
result = result * base;
There are two ways to explain this operator. One way is to simply point out the above equivalency. Whenever you see the *= operator in code, simply replace it in your mind with the equivalent assignment statement. Another way is to describe the concept behind the operator. The idea is that result *= base; means "multiply the value stored in result by base". There is also a += operator for "add and assign", a -= operator for "subtract and assign", and a /= operator for "divide and assign."
We will now try to improve our draw box program by restructuring it. Our goal here is not to change the behavior (output) of our program in any way, but to restructure it. Our main method has too much detail in it. Let's change our main function so it looks like this:
public static void main(String[] args) {
{
<declarations>
width = getWidth();
height = getHeight();
drawBox(width, height);
}
All that remains is to take the statements that used to be in main() and place them in the appropriate methods (getWidth(), getHeight(), or drawBox()). Here is the final version:
import java.util.Scanner;
public class Example {
static Scanner input = new Scanner(System.in);
public static void main(String[] args) {
int width;
int height;
width = getWidth();
height = getHeight();
drawBox(width, height);
}
public static int getWidth() {
System.out.print("Enter width: ");
return input.nextInt();
}
public static int getHeight() {
System.out.print("Enter height: ");
return input.nextInt();
}
public static void drawBox(int width, int height) {
drawHorizontalLine(width);
draw2VerticalLines(width - 2, height - 2);
drawHorizontalLine(width);
}
public static void drawHorizontalLine(int numXs) {
for (int count = 0; count < numXs; count++){
System.out.print("X");
}
System.out.println();
}
public static void draw2VerticalLines(int numSpaces, int numRows) {
for (int rowCount = 0; rowCount < numRows; rowCount++){
drawOneRow(numSpaces);
}
}
public static void drawOneRow(int numSpaces) {
System.out.print("X");
for (int spaceCount = 0; spaceCount < numSpaces; spaceCount++){
System.out.print(" ");
}
System.out.println("X");
}
}
Let's do another value-returning method example where we will use multiple return statements. At the same time we will illustrate another concept: many value-returning statements return the boolean data-type. This type of value-returning method would typically be called inside an if or while condition. For example, suppose we write a value-returning method named isdigit(). It takes a single parameter, a character, and returns true if the character is a digit, false otherwise. It might be called like this:
System.out.println("enter a character: ");
ch = input.next().charAt(0);
if (isdigit(ch)){
System.out.println("you entered a digit.");
}
It is very common for these boolean value returning methods to be named "issomething". Let's write our own boolean value-returning method named isLeapYear. First we need to know what is a leap year and what isn't. Here's the rule:
Notice that we lived through a pretty unusual year, since 2000 is divisible by 400. Was the year 2000 a leap year?
When we write our code for this method, it will actually turn out to be easier if we turn the rule above around and start with what we know for sure. We know for sure that if a year is divisible by 400, it is a leap year. After that, once we have taken care of the "divisible by 400" case, we then know for sure that if the year is divisible by 100, then it is NOT a leap year. And so on. Here's a first shot at how we would code that:
public static boolean isLeapYear(int year)
{
if (year % 400 == 0){
return true;
}
if (year % 100 == 0){
return false;
}
if (year % 4 == 0){
return true;
}
return false;
}
Notice, however, that the code in gray above is equivalent to simply
return year % 4 == 0;
If you aren't sure you believe that, check what gets returned if year is divisible by 4, and then check what gets returned if year is not divisible by 4. You will find that both code segments return the same thing in any situation, and are therefore equivalent. So, making this simplification, our code becomes:
public static boolean isLeapYear(int year)
{
if (year % 400 == 0){
return true;
}
if (year % 100 == 0){
return false;
}
return year % 4 == 0;
}
Notice that although we only want to execute the second if statement if the first condition is false, we don't need to use else here, because when Java hits the return statement the method immediately ends.