Difference between revisions of "Java 6: Design Principles"

From Deep Blue Robotics Wiki
Jump to: navigation, search
(Memory Management)
(Memory Management)
Line 198: Line 198:
 
==Memory Management==
 
==Memory Management==
  
Java has a feature called automatic garbage collection that automatically gets rid of objects once they are no longer referenced anywhere in the code. This means that Java programmers do not usually need to worry about disposing of objects once they are done with them. There are ways for objects to accidentally escape the garbage collector, but we don't need to worry about them for right now (the RoboRIO has plenty of memory). For those interested, you can read this [http://stackoverflow.com/questions/6470651/creating-a-memory-leak-with-java - Stack Overflow thread] on Java memory leaks. However, you should generally avoid leaving around references to objects that take up a lot of memory, such as images.
+
Java has a feature called automatic garbage collection that automatically gets rid of objects once they are no longer referenced anywhere in the code. This means that Java programmers do not usually need to worry about disposing of objects once they are done with them. There are ways for objects to accidentally escape the garbage collector, but we don't need to worry about them for right now (the RoboRIO has plenty of memory). For those interested, you can read this [http://stackoverflow.com/questions/6470651/creating-a-memory-leak-with-java Stack Overflow thread] on Java memory leaks. However, you should generally avoid leaving around references to objects that take up a lot of memory, such as images.
  
 
Note that some languages (like C++) do not have this functionality, so objects have to be manually disposed of. This can allow programmers to write more optimized code, but it requires more effort and makes it easier to leak memory.
 
Note that some languages (like C++) do not have this functionality, so objects have to be manually disposed of. This can allow programmers to write more optimized code, but it requires more effort and makes it easier to leak memory.
  
 
Next Lesson: [http://wiki.carlmontrobotics.org/Java_7:_Additional_Topics  Additional Topics]
 
Next Lesson: [http://wiki.carlmontrobotics.org/Java_7:_Additional_Topics  Additional Topics]

Revision as of 22:04, 1 September 2016

So far you have learned the mechanics of the Java language, but you haven't learned much about the theory behind it. In this section, you will learn why the Java language (and the object-oriented paradigm itself) was designed the way that it was, and how to use the same principles to improve your own code.

Code Reuse

One key property of good code is flexibility: the code's ability to adapt to change. This is especially important for FRC, because during competition we will usually have only a couple minutes to make changes between matches, and any mistakes could make us lose our next match. The easiest way to write flexible code is code reuse. Essentially, code with the same functionality should not be duplicated, so that changes only need to be made in one place.

Variables

The simplest way to reuse code is through variables. For example, consider this PID code: leftMotor.set(kP*error+kI*totalError+kD*(error-lastError)); rightMotor.set(kP*error+kI*totalError+kD*(error-lastError)); Now, imagine that you just realized that you forgot to divide the D term by the time interval. To fix this problem, you have to change both lines of code. Admittedly there are only two lines that need to be changed in this example, but in other situations there could be many more. leftMotor.set(kP*error+kI*totalError+kD*(error-lastError)/interval); rightMotor.set(kP*error+kI*totalError+kD*(error-lastError)/interval); On the other hand, consider the following approach: double output = kP*error+kI*totalError+kD*(error-lastError); leftMotor.set(output); rightMotor.set(output); This version might be a little bit longer (although for more complex code it will actually use up less space), but it is much easier to modify, because changes will only have to be done once.

Methods

Another opportunity for code reuse is through methods. For example, consider this drivetrain code: public void tankDrive(double left, double right) {

   leftMotor.set(left);
   rightMotor.set(right);

}

public void autoDrive(double target) {

   error = target - getCurrentDistance();
   double output = kP*error+kI*totalError+kD*(error-lastError);
   totalError += error;
   lastError = error;
   leftMotor.set(output);
   rightMotor.set(output);

} It looks fine, right? However, imagine that one of the motors on the drivetrain gets inverted somehow, so you add a minus sign in the tankDrive method to fix it: public void tankDrive(double left, double right) {

   leftMotor.set(-left);
   rightMotor.set(right);

} But what about the autoDrive method? There's a good chance that you will forget to modify autoDrive as well when you change tankDrive, so your robot is just going to spin in circles during autonomous, and everybody will blame the programmers (as usual). However, consider this version of the drivetrain code: public void tankDrive(double left, double right) {

   leftMotor.set(left);
   rightMotor.set(right);

}

public void autoDrive(double target) {

   error = target - getCurrentDistance();
   double output = kP*error+kI*totalError+kD*(error-lastError);
   totalError += error;
   lastError = error;
   tankDrive(left, right);

} Now, when you invert the value in the tankDrive method, the autoDrive method will fix itself automatically, the robot will work perfectly in autonomous, and everybody will live happily ever after.

Objects

So, what does all of this have to do with object-oriented programming? Imagine that you need PID code for a few different subsystems (drivetrain, elevator, shooter, etc). The naive approach is to write PID code in each of them: public class Elevator extends Subsystem {

   double lastError = 0;
   double totalError = 0;
   final double kP = 0.05, kI =0.01, kD = 0.02;
   
   public void setElevatorPosition(double height) {
       double output = kP*error+kI*totalError+kD*(error-lastError);
       totalError += error;
       lastError = error;
       elevatorMotor.set(output);
   }
   
   // Other methods

} Each of the other subsystems would have essentially the same code. However, what if you want to divide by the time interval like before? Now you need to redo the code in every single subsystem! However, a better approach would be to create a separate class to handle PID, and use an instance of it in each subsystem: public class PID {

   double lastError = 0;
   double totalError = 0;
   final double kP, kI, kD;
   
   public PID(double kP, double kI, double kD) {
       this.kP = kP;
       this.kI = kI;
       this.kD = kD;
   }
   public double getOutput(double input) {
       double output = kP*error+kI*totalError+kD*(error-lastError);
       totalError += error;
       lastError = error;
       return output;
   }

} public class Elevator extends Subsystem {

   PID elevatorPID = new PID(0.05, 0.01, 0.02);    
   public void setElevatorPosition(double height) {
       elevatorMotor.set(elevatorPID.getOutput(height));
   }
   
   // Other methods

} With this approach, changes to the PID code only need to be made in one place, the PID class. All of the subsystems that rely on PID can be left unchanged. As you can see, objects are a powerful tool for improving code flexibility.

Inheritance

Another way to reuse code is through inheritance. If multiple classes have a lot of the same functionality, then it can be helpful to create a superclass that contains all of their shared code. That way, changes made to the superclass will be automatically applied to all of the subclasses.

Encapsulation

Encapsulation is the principle of keeping different parts of the code separate from each other. This makes code safer, because changes to one part of the code will have no effect on the rest of it. Without encapsulation everything is interconnected, so changes to one section of code can affect all of the others.

For example, imagine that you created a command that displays the interval (the time between loops) on SmartDashboard. However, in an attempt to be more efficient, you just reuse a timer that somebody already created in the Drivetrain class instead of creating a new one. public class DisplayInterval extends Command {

   public void execute() {
       Timer timer = Robot.drivetrain.timer;
       SmartDashboard.putNumber("Interval", timer.get());
       timer.reset();
   }
   // Other methods

} However, what if somebody else then uses that same timer for their PID code? public class Drivetrain extends Subsystem {

   public Timer timer = new Timer();
   double lastError = 0;
   double totalError = 0;
   final double kP = 0.05, kI =0.01, kD = 0.02;
   public void autoDrive(double distance) {
       double interval = timer.get();
       timer.reset();
       double output = kP*error+kI*totalError+kD*(error-lastError)/interval;
       totalError += error*interval;
       lastError = error;
       tankDrive(output, output);
   }
   // Other methods

} Now, both classes are resetting the timer individually. This means that when the DisplayInterval command resets the timer each cycle, it causes the auto code to read the interval as very close to zero, which means that the D term will become huge and ruin the robot's autonomous. However, the writer of the Drivetrain class will have no idea that the timer is getting reset from somewhere else, and so they will have a very difficult time finding the problem.

This problem occurred because the DisplayInterval class violated the encapsulation principle when it directly used the timer variable in the Drivetrain class. However, even if you use proper encapsulation in your own code, how can you prevent other programmers from accidentally interfering with it? The solution is to use access modifiers. You have probably already been told to make all of your instance variables private. This purpose of this practice is to prevent other classes from accessing the fields directly, which would violate encapsulation. If they need to interact with your class, they have to go through public methods that were made specifically for that purpose.

Polymorphism

Polymorphism is the principle that the same code can have different effects on objects depending on their datatype. This both helps with code reuse and makes code more easily extensible (i.e. you can add new functionality more easily).

The simplest example of polymorphism is overloading methods. You have already learned that if multiple methods have the same name but different parameters, the correct method will be invoked based on the arguments that you give to the function. This can make code cleaner and easier to understand, but does not have very much practical effect.

A much more powerful type of polymorphism, however, involves inheritance. Recall that an instance of a subclass can be used in place of an instance of its superclass. However, the subclass may have different implementations of certain methods because of overriding, so the object itself decides which implementation of the method is called. This can be used to write code that can be applied to any subclass of a given class. For example, consider the SpeedController class. Each subclass (Talon, Jaguar, etc) has its own implementation of the set method. However, if you have an array of different SpeedControllers, you can still set all of them with the same code: SpeedController[] motors = {new Talon(1), new Victor(2), new Jaguar(3)}; for (SpeedController motor:motors) {

   motor.set(1.0);

} If the motors did not share a common superclass, the following code would be necessary: Object[] motors = {new Talon(1), new Victor(2), new Jaguar(3)}; for (Object motor:motors) {

   if (motor instanceof Talon) ((Talon)motor).set(1.0);
   if (motor instanceof Victor) ((Victor)motor).set(1.0);
   if (motor instanceof Jaguar) ((Jaguar)motor).set(1.0);

} As you can see, the example without polymorphism is much less elegant. It does not allow code reuse (e.g. setting the motors to -1.0 instead would require 3 changes instead of 1), and if a new type of motor were added (e.g. VictorSP), another line of code would need to be added to accommodate it.

Static Typing

Static typing means that each variable can only store one datatype (due to polymorphism you can actually store a subclass of that datatype as well). This prevents bugs that can result from accidentally using incorrect datatypes, and makes code more organized and easier to understand. IDEs like Eclipse can even use this property to offer suggestions for autocompleting statements. This means that Java is type safe, because variables will never turn out to be the wrong datatype at runtime. However, type mismatches can still occur if code uses explicit casts. For this reason, explicit casting should usually be avoided. Object joystick = new Joystick(0); SpeedController s = (SpeedController) joystick; // Error!

Note that not all languages use static typing. Some, like Python, use dynamic typing, which allows variables to store any datatype. A process called duck-typing lets programmers call methods on objects without knowing which class they actually belong to. The name duck-typing comes from the saying "If it looks like a duck, swims like a duck, and quacks like a duck, it probably is a duck." Essentially, if the object has the method you want to call, then it doesn't matter what class it actually belongs to. This approach has both advantages and disadvantages, and whether static or dynamic typing is superior depends on the situation (and on who you ask).

Efficiency

Some algorithms take a long time to run, while others execute much faster. To estimate the efficiency of an algorithm, count how many times your code will be executed due to recursion or for/while loops. For example, this code will have 100 cycles (10 for the outer for-loop times 10 for the inner for-loop): public void timesTable() {

   for (int i=0; i<10; i++) {
       for (int j=0; j<10; j++) {
           System.out.print(i*j+" ")
       }
       System.out.println();
   }
 }

The greater the number of cycles required, the slower the algorithm is. The amount of cycles will often be some function of the input size. For example, in this prime testing algorithm, the number of cycles will be approximately equal to the square root of n: public boolean isPrime(int n) {

   for (int factor=2; factor<Math.sqrt(n); factor++) {
       if (n%factor == 0) return false;
   }
   return true;
 }

If the number of cycles is a polynomial (n raised to a power), then the algorithm will be fairly fast (as long as the power isn't too large). However, if it has an exponential (or factorial) relationship with the size of the input, then the algorithm will take a really long time to run for large input sizes. To understand this difference, just compare the values of n2 and 2n when n equals 1000 (spoiler: the second one will probably cause an overflow error on your calculator). To learn more about this topic, see the Wikipedia articles on Big O notation and P = nP.

For our purposes in FRC, efficiency is not a huge concern because the RoboRIO is a fairly powerful processor. However, extremely slow algorithms (exponential, factorial, or large powers) can still cause problems, and should be avoided.

Memory Management

Java has a feature called automatic garbage collection that automatically gets rid of objects once they are no longer referenced anywhere in the code. This means that Java programmers do not usually need to worry about disposing of objects once they are done with them. There are ways for objects to accidentally escape the garbage collector, but we don't need to worry about them for right now (the RoboRIO has plenty of memory). For those interested, you can read this Stack Overflow thread on Java memory leaks. However, you should generally avoid leaving around references to objects that take up a lot of memory, such as images.

Note that some languages (like C++) do not have this functionality, so objects have to be manually disposed of. This can allow programmers to write more optimized code, but it requires more effort and makes it easier to leak memory.

Next Lesson: Additional Topics