Lesson 1 - Introduction to object-oriented programming in Java
Welcome to the first lesson of the object-oriented Java programming course. We finished the Java basic constructs course last time with the article about Mathematical functions in Java. In this course, you'll learn to program in an object-oriented way and will also develop an object-oriented way of thinking. It's a bit different than anything we've done until now. For starters, we will no longer treat programs like several lines of code that the interpreter executes one by one.
The invention of object-oriented programming, OOP hereinafter, was no coincidence, but the result of a development which led to its creation. It's a modern software development methodology supported by most programming languages. A common mistake is that people think that OOP should only be used for certain kinds of programs and that for other cases it would be needlessly complicated; however, the opposite has proven to be true. OOP is a philosophy. It's a new way of looking at a program and the communication between its parts. We should always use it whenever we create a simple utility or a complex database system. The OOP is not just a technology or a recommended program structure, it's mainly a new way of thinking. A new perspective from which we could analyze problems and a new era in software development.
As first, we'll look quickly into the history of how people programmed before OOP and what specific problems OOP resolves. It's important for us to fully understand why the OOP had to be created.
The evolution of methodologies
There is a big difference between programming nowadays and programming 40 years ago. The first computers didn't have great performance and their software was quite simple. The evolution of hardware is so fast that the number of transistors in microprocessors doubles every year (Moore's Law). Unfortunately, people aren't able to evolve as quickly as hardware does. Faster computers require more and more sophisticated and complex software (people want more and more from their computers). So much that at one point, we found out that about 90% of all software doesn't meet deadlines, requires additional costs or isn't finished at all. Developers started looking for new ways to write programs. Several new approaches took turns. More precisely, paradigms (ways of thinking). Listed as follows:
1. Machine code
The program was just a set of instructions where we weren't able to name variables or enter mathematical expressions. The source code was obviously specific for the then current hardware (processor). Replaced soon after it was established.
2. The unstructured paradigm
The unstructured approach is similar to the assembly languages, it's a set of instructions which are executed line by line. The source code wasn't dependent on the hardware and was human readable. This approach enabled the creation of more complex programs for a while. There were still many pitfalls: the only way to repeat something or to branch off a code was the GOTO statement. GOTO allowed to "jump" to different locations within the program. Locations were previously specified by a line number in the source code, which is obviously impractical. When we add a new line of code, the numbers no longer match and the code is broken. Later, it was made possible to define what they then called "labels". This approach served as a way of simulating loops. This method of writing programs is of course very confusing and soon failed to be sufficient for the development of complex programs.
Consider that the huge adoption of personal computers over the past few decades caused the growth of software demand and, naturally, a demand of more people who make programs (programmers). Certainly, there are people who can write bulletproof programs even in the ASM or other low-level languages, but how many are there? How much does this superhuman job cost? It's necessary to write programs in a way that even less experienced programmers could write high-quality programs and not need to go through 5 years of experience to achieve the same results.
3. The structured programming
Structured programming is the first paradigm that lasted for a longer time and was quite sufficient for the development of new programs. They would mostly program using loops and branching. Conceptually, we are in the structured programming era based on what we have learned from the first course.
The program would be decomposed into functions, methods, that we haven't discussed yet since Java, which is an object-oriented language, doesn't even allow us to declare them. There is a way to do it anyway, but I'd rather skip this intermediate step and get right into OOP. In structured programming, there is a recurrent theme of functional decomposition. A problem is decomposed into several subproblems and each subproblem is then solved with some parametrized function. The disadvantage to it is that a function can only do one thing and when we want a different behavior, we have to write a new one. There is no way to reuse an old code and modify it. We need to write it again and again - it creates unnecessary, potentially costly, errors. This disadvantage can be partially worked around by using parametrized functions or using global variables. Which is still inconvenient because the number of parameters could end up being in enormous quantities.
Another pitfall with global data is that functions can access the data of other functions. This is the beginning of the end, we can't guarantee that global data isn't being overwritten somewhere between functions. It leads to uncontrollable problems. The entire program will consist of unencapsulated code blocks and can hardly be maintained. Any modification increases the complexity of the program, and then the program will necessarily come to a situation where the cost of adding new features will overbalance the value added by these features. Languages using this approach are, for example, the C language and Pascal.
Between structured programming and the object-oriented programming, there was one more intermediate approach called modular programming. It involved the encapsulation of specific functionality into modules. Regardless, there was no way to modify and reuse an already written code.
As I mentioned at the beginning of the article, it's sometimes said that simple programs mustn't be written in an object-oriented way, but structural, which isn't true. If we program in a structural fashion, we will end up making a blob of uncertainty that will be almost unreadable by most people. Then again, it would come to a point where the program wouldn't even be upgradable and we'd either have to throw it away or rewrite it using the OOP.
The non-object-oriented methods of writing code are called "spaghetti code" because of their lack of clarity (everything is tangled together like spaghetti).
The object-oriented approach
OOP is a philosophy and a way of thinking, designing and implementing solutions that focus on reusability. This approach is inspired by the industrial revolution - the invention of basic components. For example, when we build our house, we don't burn our own bricks and forge the nails, we order them.
Making a "component program" is smarter and cheaper. Components do not fail, if there's a problem, it is most likely in the code you have written in one specific section. We're motivated to write clear code since it can be used by others or by ourselves in other projects. Let's face it, humans are lazy by nature and if we thought that our code wouldn't ever be reused, we wouldn't write it in the first place ).
Of course, we'll use the knowledge we have gained until now. The main difference is that now, our code will be structured differently into multiple communicating objects.
How the OOP works
It tries to simulate reality as we're used to perceiving it. We can say that we recede from how the program is seen by the computer (machine) and write it from the programmer's (human's) point of view. As we had replaced the assembler with human-readable mathematical notations, now we're going even further and replace those, too. OOP is, therefore, a certain level of abstraction above the program. This has significant advantages because it is more natural and clear to us.
The basic unit is the object which corresponds with some object from the real world, e.g. a human object or a database object).
The object has attributes and methods.
Object attributes are properties or data that it stores, e.g. the human's name and age. For a database object it could be the password or whatever is needed to make the object more useful. It's a simple variable with which we have worked a hundred times. Sometimes they're called the "object's internal state".
Object variables are called "fields" in Java. However, other languages often call them properties (PHP) or attributes and I might call them attributes as well in further texts.*
Methods are abilities that the object can perform. Human, for example, could have the methods goToWork(), greet() or blink(). For the database, it could be addEntry() or search(). Methods can have parameters and can also return values. You've actually used them before without even knowing! Remember the split() method on the String object? A String is actually an object that represents text. You can see that we can easily imagine that we're dealing with text. We can modify it, ask it to return its length, combine it with other strings, etc.. It contains methods that a text can perform, copying, deleting, splitting, among others, and also has fields, e.g. length which contains its length.
In older languages, methods didn't belong to objects but were loosely placed in modules (units). We could call them by typing split(text) instead of text.split(). The disadvantage to that was, of course, that the split() method didn't belong to anything. There was no way to list what String could do and the code was messy. Additionally, we couldn't have two methods with the same name. In OOP we can have both user.remove() and article.remove(). It's very clear and simple. In a structured program we'd have to write: remove_user(user) and remove_article(article). We'd have to create thousands of silly, unnecessary methods. If you're thinking, hey, isn't that what the PHP language does? You are absolutely right. PHP is terrible when it comes to things like this, and for that same reason, its design is considered old. It became fully object-oriented later, but its foundations will probably never change. Java is a modern language and its libraries are strongly built on objects.
In this article, we're going to explain the basics of how to create objects and how to encapsulate their internal logic. Other OOP features, mainly inheritance, will be explained in the following lessons, so as to not overload your brain
We have already encountered the term "class". We understood it as a set of commands. A class, however, allows us to do so much more. A class is a pattern that we use to create objects. It defines their properties and abilities.
An object created according to a class is called the instance. Instances have the same interface as the class according to which they were created, but they mutually differ in their internal data (fields). For example, let's make a Human class and create the Carl and Jack instances from it. Both instances have the same fields as the class (e.g. name and age) and methods (goToWork() and greet()), but the values in them are different. The first instance has the value "Carl" in the name attribute and 22 in age, the second "Jack" and 45.
The communication between objects is performed by messaging which makes the syntax clear. The message usually looks like this: recipient.methodName(parameters). For example, carl.greet(neighbor) could cause the Carl instance to greet the neighbor instance.
OOP is consists of three different concepts:
Let's look into the first of them:
Encapsulation allows us to hide some methods and fields so they can remain available only from inside of the class. The object can be thought of as a black box that provides an interface through which we can pass instructions/data to be processed by it.
We don't know how the object works internally, but we know how it behaves on the outside and how we should use it. We can't cause errors because we are only allowed to use in a way its creator meant it to be used.
An example might be the Human class having a birthDate as other fields based on this data: fullAged and age. If someone changed BirthDate from outside the object, the values in fullAged and age variables could become invalid. Which means that the internal state of the object must have been inconsistent. This could happen to us in structured programming. In OOP, however, we encapsulate the object and mark the birthDate attribute as private so it won't be visible from the outside. To the outside, we'd provide a changeBirthDate() method which would store a new birth date into a birthDate variable and also perform a necessary age re-calculation and full-age re-valuation. Usage of the object is always safe and the application is always stable.
Encapsulation forces programmers to use an object only in the right way. A class interface is divided into publicly accessible (public) and internal structures (private).
In the next lesson, First object-oriented app in Java - Hello object world, we'll create our first object-oriented program.
No one has commented yet - be the first!